Repository: jclouds-labs
Updated Branches:
  refs/heads/master d74d7f62d -> a5dbf0065


http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/main/java/org/jclouds/aliyun/ecs/functions/ArrayToCommaSeparatedString.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/main/java/org/jclouds/aliyun/ecs/functions/ArrayToCommaSeparatedString.java
 
b/aliyun-ecs/src/main/java/org/jclouds/aliyun/ecs/functions/ArrayToCommaSeparatedString.java
new file mode 100644
index 0000000..3bb22b4
--- /dev/null
+++ 
b/aliyun-ecs/src/main/java/org/jclouds/aliyun/ecs/functions/ArrayToCommaSeparatedString.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.aliyun.ecs.functions;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Iterables;
+
+import javax.inject.Singleton;
+import java.util.Arrays;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Takes an array of string and return a "["s1", "s2", … "sN"]"
+ */
+@Singleton
+public class ArrayToCommaSeparatedString implements Function<Object, String> {
+   @Override
+   public String apply(Object input) {
+      checkArgument(checkNotNull(input, "input") instanceof String[], "This 
function is only valid for array of Strings!");
+      String[] names = (String[]) input;
+
+      String arrayToCommaSeparatedString = Joiner.on(",")
+              .join(Iterables.transform(Arrays.asList(names), new 
Function<String, String>() {
+                 @Override
+                 public String apply(String s) {
+                    return new StringBuilder(s.length() + 
1).append('"').append(s).append('"').toString();
+                 }
+              }));
+      return String.format("[%s]", arrayToCommaSeparatedString);
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/main/java/org/jclouds/aliyun/ecs/functions/BaseToPagedIterable.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/main/java/org/jclouds/aliyun/ecs/functions/BaseToPagedIterable.java
 
b/aliyun-ecs/src/main/java/org/jclouds/aliyun/ecs/functions/BaseToPagedIterable.java
deleted file mode 100644
index 9f2ddcf..0000000
--- 
a/aliyun-ecs/src/main/java/org/jclouds/aliyun/ecs/functions/BaseToPagedIterable.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * 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.aliyun.ecs.functions;
-
-import com.google.common.base.Function;
-import com.google.common.base.Optional;
-import org.jclouds.aliyun.ecs.ECSComputeServiceApi;
-import org.jclouds.aliyun.ecs.domain.options.ListImagesOptions;
-import org.jclouds.collect.IterableWithMarker;
-import org.jclouds.collect.internal.Arg0ToPagedIterable;
-
-/**
- * Base class to implement the functions that build the
- * <code>PagedIterable</code>. Subclasses just need to override the
- * {@link #fetchPageUsingOptions(ListImagesOptions, Optional)} to invoke the 
right API
- * method with the given options parameter to get the next page.
- */
-public abstract class BaseToPagedIterable<T, O extends ListImagesOptions> 
extends
-        Arg0ToPagedIterable<T, BaseToPagedIterable<T, O>> {
-   private final Function<Integer, O> pageNumberToOptions;
-   protected final ECSComputeServiceApi api;
-
-   protected BaseToPagedIterable(ECSComputeServiceApi api, Function<Integer, 
O> pageNumberToOptions) {
-      this.api = api;
-      this.pageNumberToOptions = pageNumberToOptions;
-   }
-
-   protected abstract IterableWithMarker<T> fetchPageUsingOptions(O options, 
Optional<Object> arg0);
-
-   @Override
-   protected Function<Object, IterableWithMarker<T>> markerToNextForArg0(final 
Optional<Object> arg0) {
-      return new Function<Object, IterableWithMarker<T>>() {
-         @Override
-         public IterableWithMarker<T> apply(Object input) {
-            O nextOptions = 
pageNumberToOptions.apply(Integer.class.cast(input));
-            return fetchPageUsingOptions(nextOptions, arg0);
-         }
-      };
-   }
-
-}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/ImageApiLiveTest.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/ImageApiLiveTest.java
 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/ImageApiLiveTest.java
index 957d9cc..5fe5c3f 100644
--- 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/ImageApiLiveTest.java
+++ 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/ImageApiLiveTest.java
@@ -20,12 +20,14 @@ import com.google.common.base.Predicate;
 import com.google.common.collect.Iterables;
 import 
org.jclouds.aliyun.ecs.compute.internal.BaseECSComputeServiceApiLiveTest;
 import org.jclouds.aliyun.ecs.domain.Image;
-import org.jclouds.aliyun.ecs.domain.Regions;
+import org.jclouds.aliyun.ecs.domain.internal.Regions;
 import org.jclouds.aliyun.ecs.features.ImageApi;
 import org.testng.annotations.Test;
 
 import java.util.concurrent.atomic.AtomicInteger;
 
+import static 
org.jclouds.aliyun.ecs.domain.options.ListImagesOptions.Builder.imageIds;
+import static 
org.jclouds.aliyun.ecs.domain.options.PaginationOptions.Builder.pageNumber;
 import static org.testng.Assert.assertTrue;
 import static org.testng.util.Strings.isNullOrEmpty;
 
@@ -38,12 +40,27 @@ public class ImageApiLiveTest extends 
BaseECSComputeServiceApiLiveTest {
          @Override
          public boolean apply(Image input) {
             found.incrementAndGet();
-            return !isNullOrEmpty(input.imageId());
+            return !isNullOrEmpty(input.id());
          }
       }), "All images must have the 'id' field populated");
       assertTrue(found.get() > 0, "Expected some image to be returned");
    }
 
+   public void testListWithOptions() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertTrue(api().list(Regions.EU_CENTRAL_1.getName(),
+              imageIds("debian_8_09_64_20G_alibase_20170824.vhd")
+              .paginationOptions(pageNumber(3)))
+              .firstMatch(new Predicate<Image>() {
+                 @Override
+                 public boolean apply(Image input) {
+                    found.incrementAndGet();
+                    return !isNullOrEmpty(input.id());
+                 }
+              }).isPresent(), "All images must have the 'id' field populated");
+      assertTrue(found.get() > 0, "Expected some image to be returned");
+   }
+
    private ImageApi api() {
       return api.imageApi();
    }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/ImageApiMockTest.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/ImageApiMockTest.java
 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/ImageApiMockTest.java
index b333e6d..e61b910 100644
--- 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/ImageApiMockTest.java
+++ 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/ImageApiMockTest.java
@@ -19,8 +19,7 @@ package org.jclouds.aliyun.ecs.compute.features;
 import com.google.common.collect.ImmutableMap;
 import 
org.jclouds.aliyun.ecs.compute.internal.BaseECSComputeServiceApiMockTest;
 import org.jclouds.aliyun.ecs.domain.Image;
-import org.jclouds.aliyun.ecs.domain.Regions;
-import org.jclouds.collect.IterableWithMarker;
+import org.jclouds.aliyun.ecs.domain.internal.Regions;
 import org.testng.annotations.Test;
 
 import static com.google.common.collect.Iterables.isEmpty;
@@ -54,15 +53,15 @@ public class ImageApiMockTest extends 
BaseECSComputeServiceApiMockTest {
 
    public void testListImagesWithOptions() throws InterruptedException {
       server.enqueue(jsonResponse("/images-first.json"));
-      IterableWithMarker<Image> images = 
api.imageApi().list(Regions.EU_CENTRAL_1.getName(), 
paginationOptions(pageNumber(1)));
+      Iterable<Image> images = 
api.imageApi().list(Regions.EU_CENTRAL_1.getName(), 
paginationOptions(pageNumber(1)));
       assertEquals(size(images), 10);
       assertEquals(server.getRequestCount(), 1);
-      assertSent(server, "GET", "DescribeImages", ImmutableMap.of("RegionId", 
Regions.EU_CENTRAL_1.getName()), 1);
+      assertSent(server, "GET", "DescribeImages", ImmutableMap.of("RegionId", 
Regions.EU_CENTRAL_1.getName()));
    }
 
    public void testListImagesWithOptionsReturns404() throws 
InterruptedException {
       server.enqueue(response404());
-      IterableWithMarker<Image> images = 
api.imageApi().list(Regions.EU_CENTRAL_1.getName(), 
paginationOptions(pageNumber(2)));
+      Iterable<Image> images = 
api.imageApi().list(Regions.EU_CENTRAL_1.getName(), 
paginationOptions(pageNumber(2)));
       assertTrue(isEmpty(images));
       assertEquals(server.getRequestCount(), 1);
       assertSent(server, "GET", "DescribeImages", ImmutableMap.of("RegionId", 
Regions.EU_CENTRAL_1.getName()), 2);

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/RegionAndZoneApiLiveTest.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/RegionAndZoneApiLiveTest.java
 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/RegionAndZoneApiLiveTest.java
index 91239a1..f32399b 100644
--- 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/RegionAndZoneApiLiveTest.java
+++ 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/RegionAndZoneApiLiveTest.java
@@ -38,7 +38,7 @@ public class RegionAndZoneApiLiveTest extends 
BaseECSComputeServiceApiLiveTest {
          @Override
          public boolean apply(Region input) {
             found.incrementAndGet();
-            return !isNullOrEmpty(input.regionId());
+            return !isNullOrEmpty(input.id());
          }
       }), "All regions must have the 'id' field populated");
       assertTrue(found.get() > 0, "Expected some region to be returned");
@@ -50,7 +50,7 @@ public class RegionAndZoneApiLiveTest extends 
BaseECSComputeServiceApiLiveTest {
          @Override
          public boolean apply(Zone input) {
             found.incrementAndGet();
-            return !isNullOrEmpty(input.zoneId());
+            return !isNullOrEmpty(input.id());
          }
       }), "All zones must have the 'id' field populated");
       assertTrue(found.get() > 0, "Expected some zone to be returned");

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/RegionAndZoneApiMockTest.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/RegionAndZoneApiMockTest.java
 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/RegionAndZoneApiMockTest.java
index 291d1b0..e116845 100644
--- 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/RegionAndZoneApiMockTest.java
+++ 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/RegionAndZoneApiMockTest.java
@@ -19,7 +19,7 @@ package org.jclouds.aliyun.ecs.compute.features;
 import com.google.common.collect.ImmutableMap;
 import 
org.jclouds.aliyun.ecs.compute.internal.BaseECSComputeServiceApiMockTest;
 import org.jclouds.aliyun.ecs.domain.Region;
-import org.jclouds.aliyun.ecs.domain.Regions;
+import org.jclouds.aliyun.ecs.domain.internal.Regions;
 import org.jclouds.aliyun.ecs.domain.Zone;
 import org.testng.annotations.Test;
 

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SecurityGroupApiLiveTest.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SecurityGroupApiLiveTest.java
 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SecurityGroupApiLiveTest.java
new file mode 100644
index 0000000..5c76fc5
--- /dev/null
+++ 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SecurityGroupApiLiveTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.aliyun.ecs.compute.features;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import 
org.jclouds.aliyun.ecs.compute.internal.BaseECSComputeServiceApiLiveTest;
+import org.jclouds.aliyun.ecs.domain.IpProtocol;
+import org.jclouds.aliyun.ecs.domain.Permission;
+import org.jclouds.aliyun.ecs.domain.internal.Regions;
+import org.jclouds.aliyun.ecs.domain.SecurityGroup;
+import org.jclouds.aliyun.ecs.domain.SecurityGroupRequest;
+import org.jclouds.aliyun.ecs.domain.options.CreateSecurityGroupOptions;
+import org.jclouds.aliyun.ecs.features.SecurityGroupApi;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.util.Strings.isNullOrEmpty;
+
+@Test(groups = "live", testName = "SecurityGroupApiLiveTest")
+public class SecurityGroupApiLiveTest extends BaseECSComputeServiceApiLiveTest 
{
+
+   public static final String TEST_PORT_RANGE = "8081/8085";
+   public static final String INTERNET = "0.0.0.0/0";
+
+   private String securityGroupId;
+
+   @BeforeClass
+   public void setUp() {
+      SecurityGroupRequest request = 
api().create(Regions.EU_CENTRAL_1.getName(),
+            CreateSecurityGroupOptions.Builder
+                  .securityGroupName("jclouds-test")
+      );
+      securityGroupId = request.getSecurityGroupId();
+   }
+
+   @AfterClass
+   public void tearDown() {
+      if (securityGroupId != null) {
+         api().delete(Regions.EU_CENTRAL_1.getName(), securityGroupId);
+      }
+   }
+
+   public void testAddRules() {
+      api().addInboundRule(Regions.EU_CENTRAL_1.getName(), securityGroupId, 
IpProtocol.TCP, TEST_PORT_RANGE, INTERNET);
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testAddRules")
+   public void testGet() {
+      Permission permission = 
Iterables.getOnlyElement(api().get(Regions.EU_CENTRAL_1.getName(), 
securityGroupId));
+      checkPermission(permission);
+   }
+
+   @Test(groups = "live", dependsOnMethods = "testGet")
+   public void testList() {
+      final AtomicInteger found = new AtomicInteger(0);
+      
assertTrue(Iterables.all(api().list(Regions.EU_CENTRAL_1.getName()).concat(), 
new Predicate<SecurityGroup>() {
+         @Override
+         public boolean apply(SecurityGroup input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.id());
+         }
+      }), "All security groups must have the 'id' field populated");
+      assertTrue(found.get() > 0, "Expected some security group to be 
returned");
+   }
+
+   private SecurityGroupApi api() {
+      return api.securityGroupApi();
+   }
+
+   private void checkPermission(Permission permission) {
+      assertNotNull(permission.ipProtocol());
+      assertNotNull(permission.portRange());
+      assertNotNull(permission.sourceCidrIp());
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SecurityGroupApiMockTest.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SecurityGroupApiMockTest.java
 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SecurityGroupApiMockTest.java
new file mode 100644
index 0000000..46aa76b
--- /dev/null
+++ 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SecurityGroupApiMockTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.aliyun.ecs.compute.features;
+
+import com.google.common.collect.ImmutableMap;
+import 
org.jclouds.aliyun.ecs.compute.internal.BaseECSComputeServiceApiMockTest;
+import org.jclouds.aliyun.ecs.domain.internal.Regions;
+import org.jclouds.aliyun.ecs.domain.SecurityGroup;
+import org.testng.annotations.Test;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static 
org.jclouds.aliyun.ecs.domain.options.ListSecurityGroupsOptions.Builder.paginationOptions;
+import static 
org.jclouds.aliyun.ecs.domain.options.PaginationOptions.Builder.pageNumber;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+@Test(groups = "unit", testName = "SecurityGroupApiMockTest", singleThreaded = 
true)
+public class SecurityGroupApiMockTest extends BaseECSComputeServiceApiMockTest 
{
+
+   public void testListSecurityGroups() throws InterruptedException {
+      server.enqueue(jsonResponse("/securitygroups-first.json"));
+      server.enqueue(jsonResponse("/securitygroups-last.json"));
+      Iterable<SecurityGroup> securitygroups = 
api.securityGroupApi().list(Regions.EU_CENTRAL_1.getName()).concat();
+      assertEquals(size(securitygroups), 7); // Force the PagedIterable to 
advance
+      assertEquals(server.getRequestCount(), 2);
+      assertSent(server, "GET", "DescribeSecurityGroups", 
ImmutableMap.of("RegionId", Regions.EU_CENTRAL_1.getName()));
+      assertSent(server, "GET", "DescribeSecurityGroups", 
ImmutableMap.of("RegionId", Regions.EU_CENTRAL_1.getName()), 2);
+   }
+
+   public void testListSecurityGroupsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+      Iterable<SecurityGroup> securitygroups = 
api.securityGroupApi().list(Regions.EU_CENTRAL_1.getName()).concat();
+      assertTrue(isEmpty(securitygroups));
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "DescribeSecurityGroups", 
ImmutableMap.of("RegionId", Regions.EU_CENTRAL_1.getName()));
+   }
+
+   public void testListSecurityGroupsWithOptions() throws InterruptedException 
{
+      server.enqueue(jsonResponse("/securitygroups-first.json"));
+      Iterable<SecurityGroup> securitygroups = 
api.securityGroupApi().list(Regions.EU_CENTRAL_1.getName(), 
paginationOptions(pageNumber(1).pageSize(5)));
+      assertEquals(size(securitygroups), 5);
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "DescribeSecurityGroups", 
ImmutableMap.of("RegionId", Regions.EU_CENTRAL_1.getName()), 1);
+   }
+
+   public void testListSecurityGroupsWithOptionsReturns404() throws 
InterruptedException {
+      server.enqueue(response404());
+      Iterable<SecurityGroup> securitygroups = 
api.securityGroupApi().list(Regions.EU_CENTRAL_1.getName(), 
paginationOptions(pageNumber(2)));
+      assertTrue(isEmpty(securitygroups));
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "DescribeSecurityGroups", 
ImmutableMap.of("RegionId", Regions.EU_CENTRAL_1.getName()), 2);
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SshKeyPairApiLiveTest.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SshKeyPairApiLiveTest.java
 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SshKeyPairApiLiveTest.java
new file mode 100644
index 0000000..85ceee5
--- /dev/null
+++ 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SshKeyPairApiLiveTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.aliyun.ecs.compute.features;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import 
org.jclouds.aliyun.ecs.compute.internal.BaseECSComputeServiceApiLiveTest;
+import org.jclouds.aliyun.ecs.domain.KeyPair;
+import org.jclouds.aliyun.ecs.domain.KeyPairRequest;
+import org.jclouds.aliyun.ecs.domain.internal.Regions;
+import org.jclouds.aliyun.ecs.features.SshKeyPairApi;
+import org.jclouds.ssh.SshKeys;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.util.Strings.isNullOrEmpty;
+
+@Test(groups = "live", testName = "SecurityGroupApiLiveTest")
+public class SshKeyPairApiLiveTest extends BaseECSComputeServiceApiLiveTest {
+
+   private String keyPairName = "jclouds-test";
+
+   @BeforeClass
+   public void setUp() {
+      KeyPairRequest request = api().create(Regions.EU_CENTRAL_1.getName(), 
keyPairName);
+      assertNotNull(request.getRequestId());
+   }
+
+   @AfterClass
+   public void tearDown() {
+      if (keyPairName != null) {
+         api().delete(Regions.EU_CENTRAL_1.getName(), keyPairName);
+      }
+   }
+
+   public void testImport() {
+      String importedKeyPairName = keyPairName  + new Random().nextInt(1024);
+      KeyPair imported = api().importKeyPair(
+              Regions.EU_CENTRAL_1.getName(),
+              SshKeys.generate().get("public"),
+              importedKeyPairName);
+      assertEquals(imported.name(), importedKeyPairName);
+      assertNotNull(imported.privateKeyBody());
+      assertNotNull(imported.keyPairFingerPrint());
+   }
+
+   public void testList() {
+      final AtomicInteger found = new AtomicInteger(0);
+      
assertTrue(Iterables.all(api().list(Regions.EU_CENTRAL_1.getName()).concat(), 
new Predicate<KeyPair>() {
+         @Override
+         public boolean apply(KeyPair input) {
+            found.incrementAndGet();
+            return !isNullOrEmpty(input.name());
+         }
+      }), "All key pairs must have the 'name' field populated");
+      assertTrue(found.get() > 0, "Expected some key pair to be returned");
+   }
+
+   private SshKeyPairApi api() {
+      return api.sshKeyPairApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SshKeyPairApiMockTest.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SshKeyPairApiMockTest.java
 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SshKeyPairApiMockTest.java
new file mode 100644
index 0000000..23c21ad
--- /dev/null
+++ 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/SshKeyPairApiMockTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.aliyun.ecs.compute.features;
+
+import com.google.common.collect.ImmutableMap;
+import 
org.jclouds.aliyun.ecs.compute.internal.BaseECSComputeServiceApiMockTest;
+import org.jclouds.aliyun.ecs.domain.KeyPair;
+import org.jclouds.aliyun.ecs.domain.KeyPairRequest;
+import org.jclouds.aliyun.ecs.domain.internal.Regions;
+import org.jclouds.aliyun.ecs.domain.Request;
+import org.jclouds.aliyun.ecs.domain.options.ListKeyPairsOptions;
+import org.jclouds.aliyun.ecs.domain.options.PaginationOptions;
+import org.jclouds.collect.IterableWithMarker;
+import org.testng.annotations.Test;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+@Test(groups = "unit", testName = "SshKeyPairApiMockTest", singleThreaded = 
true)
+public class SshKeyPairApiMockTest extends BaseECSComputeServiceApiMockTest {
+
+   public void testCreateSshKey() throws InterruptedException {
+      server.enqueue(jsonResponse("/keypair-create-res.json"));
+      KeyPairRequest keyPairRequest = 
api.sshKeyPairApi().create(Regions.EU_CENTRAL_1.getName(), "jclouds");
+      assertEquals(keyPairRequest, 
objectFromResource("/keypair-create-res.json", KeyPairRequest.class));
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "CreateKeyPair", ImmutableMap.of("RegionId", 
Regions.EU_CENTRAL_1.getName()));
+   }
+
+   public void testDeleteSshKey() throws InterruptedException {
+      server.enqueue(jsonResponse("/keypair-delete-res.json"));
+      Request delete = 
api.sshKeyPairApi().delete(Regions.EU_CENTRAL_1.getName());
+      assertEquals(delete, objectFromResource("/keypair-delete-res.json", 
Request.class));
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "DeleteKeyPairs", ImmutableMap.of("RegionId", 
Regions.EU_CENTRAL_1.getName()));
+   }
+
+   public void testImportSshKey() throws InterruptedException {
+      server.enqueue(jsonResponse("/keypair-import-res.json"));
+      KeyPair keyPair = api.sshKeyPairApi().importKeyPair(
+              Regions.EU_CENTRAL_1.getName(),
+                            "ssh-rsa 
AAAAB3NzaC1yc2EAAAADAQABAAABAQCdgcoNzH4hCc0j3b4MuG503L/J54uyFvwCAOu8vSsYuLpJ4AEyEOv+T0SfdF605fK6GYXA16Rxk3lrPt7mfKGNtXR0Ripbv7Zc6PvCRorwgj/cjh/45miozjrkXAiHD1GFZycfbi4YsoWAqZj7W4mwtctmhrYM0FPdya2XoRpVy89N+A5Xo4Xtd6EZn6JGEKQM5+kF2aL3ggy0od/DqjuEVYwZoyTe1RgUTXZSU/Woh7WMhsRHbqd3eYz4s6ac8n8IJPGKtUaQeqUtH7OK6NRYXVypUrkqNlwdNYZAwrjXg/x5T3D+bo11LENASRt9OJ2OkmRSTqRxBeDkhnVauWK/",
+              "jclouds"
+      );
+      assertEquals(keyPair, objectFromResource("/keypair-import-res.json", 
KeyPair.class));
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "POST", "ImportKeyPair", ImmutableMap.of("RegionId", 
Regions.EU_CENTRAL_1.getName()));
+   }
+
+   public void testListImages() throws InterruptedException {
+      server.enqueue(jsonResponse("/keypairs-first.json"));
+      server.enqueue(jsonResponse("/keypairs-last.json"));
+
+      Iterable<KeyPair> keypairs = 
api.sshKeyPairApi().list(Regions.EU_CENTRAL_1.getName()).concat();
+      assertEquals(size(keypairs), 12);
+      assertEquals(server.getRequestCount(), 2);
+      assertSent(server, "GET", "DescribeKeyPairs");
+      assertSent(server, "GET", "DescribeKeyPairs", 2);
+   }
+
+   public void testListKeyPairsReturns404() {
+      server.enqueue(response404());
+      Iterable<KeyPair> keypairs = 
api.sshKeyPairApi().list(Regions.EU_CENTRAL_1.getName()).concat();
+      assertTrue(isEmpty(keypairs));
+      assertEquals(server.getRequestCount(), 1);
+   }
+
+   public void testListKeyPairsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/keypairs-first.json"));
+
+      IterableWithMarker<KeyPair> keypairs = 
api.sshKeyPairApi().list(Regions.EU_CENTRAL_1.getName(), 
ListKeyPairsOptions.Builder
+              .paginationOptions(PaginationOptions.Builder.pageNumber(1)));
+
+      assertEquals(size(keypairs), 10);
+      assertEquals(server.getRequestCount(), 1);
+
+      assertSent(server, "GET", "DescribeKeyPairs", 1);
+   }
+
+   public void testListKeyPairsWithOptionsReturns404() throws 
InterruptedException {
+      server.enqueue(response404());
+
+      IterableWithMarker<KeyPair> keypairs = 
api.sshKeyPairApi().list(Regions.EU_CENTRAL_1.getName(), 
ListKeyPairsOptions.Builder
+              .paginationOptions(PaginationOptions.Builder.pageNumber(2)));
+
+      assertTrue(isEmpty(keypairs));
+
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "DescribeKeyPairs", 2);
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/TagApiLiveTest.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/TagApiLiveTest.java
 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/TagApiLiveTest.java
new file mode 100644
index 0000000..854b494
--- /dev/null
+++ 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/TagApiLiveTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.aliyun.ecs.compute.features;
+
+import com.google.common.base.Predicate;
+import 
org.jclouds.aliyun.ecs.compute.internal.BaseECSComputeServiceApiLiveTest;
+import org.jclouds.aliyun.ecs.domain.internal.Regions;
+import org.jclouds.aliyun.ecs.domain.Request;
+import org.jclouds.aliyun.ecs.domain.SecurityGroupRequest;
+import org.jclouds.aliyun.ecs.domain.Tag;
+import org.jclouds.aliyun.ecs.domain.options.CreateSecurityGroupOptions;
+import org.jclouds.aliyun.ecs.domain.options.ListTagsOptions;
+import org.jclouds.aliyun.ecs.domain.options.TagOptions;
+import org.jclouds.aliyun.ecs.features.TagApi;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+import static org.testng.util.Strings.isNullOrEmpty;
+
+@Test(groups = "live", testName = "TagApiLiveTest")
+public class TagApiLiveTest extends BaseECSComputeServiceApiLiveTest {
+
+   public static final String RESOURCE_TYPE = "securitygroup";
+
+   private String securityGroupName = "pre-test-security";
+   private String securityGroupId;
+
+   @BeforeClass
+   public void setUp() {
+      SecurityGroupRequest preRequisite = 
api.securityGroupApi().create(Regions.EU_CENTRAL_1.getName(),
+              
CreateSecurityGroupOptions.Builder.securityGroupName(securityGroupName)
+      );
+      securityGroupId = preRequisite.getSecurityGroupId();
+      Request request = api().add(Regions.EU_CENTRAL_1.getName(), 
securityGroupId, RESOURCE_TYPE,
+              TagOptions.Builder.tag(1, "owner"));
+      assertNotNull(request.getRequestId());
+   }
+
+   @AfterClass
+   public void tearDown() {
+      api().remove(Regions.EU_CENTRAL_1.getName(), securityGroupId, 
RESOURCE_TYPE);
+      if (securityGroupId != null) {
+         api.securityGroupApi().delete(Regions.EU_CENTRAL_1.getName(), 
securityGroupId);
+      }
+   }
+
+   public void testList() {
+      final AtomicInteger found = new AtomicInteger(0);
+      assertFalse(api().list(Regions.EU_CENTRAL_1.getName(), 
ListTagsOptions.Builder.resourceId(securityGroupId))
+              .filter(new Predicate<Tag>() {
+                 @Override
+                 public boolean apply(Tag input) {
+                    found.incrementAndGet();
+                    return !isNullOrEmpty(input.tagKey());
+                 }
+              }).isEmpty(), "All tags must have the 'key' field populated");
+      assertTrue(found.get() > 0, "Expected some tags to be returned");
+   }
+
+   private TagApi api() {
+      return api.tagApi();
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/TagApiMockTest.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/TagApiMockTest.java
 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/TagApiMockTest.java
new file mode 100644
index 0000000..deb492d
--- /dev/null
+++ 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/features/TagApiMockTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.aliyun.ecs.compute.features;
+
+import com.google.common.collect.ImmutableMap;
+import 
org.jclouds.aliyun.ecs.compute.internal.BaseECSComputeServiceApiMockTest;
+import org.jclouds.aliyun.ecs.domain.internal.Regions;
+import org.jclouds.aliyun.ecs.domain.Tag;
+import org.testng.annotations.Test;
+
+import static com.google.common.collect.Iterables.isEmpty;
+import static com.google.common.collect.Iterables.size;
+import static 
org.jclouds.aliyun.ecs.domain.options.ListTagsOptions.Builder.paginationOptions;
+import static 
org.jclouds.aliyun.ecs.domain.options.PaginationOptions.Builder.pageNumber;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+@Test(groups = "unit", testName = "TagApiMockTest", singleThreaded = true)
+public class TagApiMockTest extends BaseECSComputeServiceApiMockTest {
+
+   public void testListTags() throws InterruptedException {
+      server.enqueue(jsonResponse("/tags-first.json"));
+      server.enqueue(jsonResponse("/tags-last.json"));
+      Iterable<Tag> tags = 
api.tagApi().list(Regions.EU_CENTRAL_1.getName()).concat();
+      assertEquals(size(tags), 10); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 2);
+      assertSent(server, "GET", "DescribeTags", ImmutableMap.of("RegionId", 
Regions.EU_CENTRAL_1.getName()));
+      assertSent(server, "GET", "DescribeTags", ImmutableMap.of("RegionId", 
Regions.EU_CENTRAL_1.getName()), 2);
+   }
+
+   public void testListTagsReturns404() throws InterruptedException {
+      server.enqueue(response404());
+      Iterable<Tag> tags = 
api.tagApi().list(Regions.EU_CENTRAL_1.getName()).concat();
+      assertTrue(isEmpty(tags));
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "DescribeTags", ImmutableMap.of("RegionId", 
Regions.EU_CENTRAL_1.getName()));
+   }
+
+   public void testListTagsWithOptions() throws InterruptedException {
+      server.enqueue(jsonResponse("/tags-first.json"));
+      Iterable<Tag> tags = api.tagApi().list(Regions.EU_CENTRAL_1.getName(), 
paginationOptions(pageNumber(1).pageSize(5)));
+      assertEquals(size(tags), 8);
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "DescribeTags", ImmutableMap.of("RegionId", 
Regions.EU_CENTRAL_1.getName()), 1);
+   }
+
+   public void testListTagsWithOptionsReturns404() throws InterruptedException 
{
+      server.enqueue(response404());
+      Iterable<Tag> tags = api.tagApi().list(Regions.EU_CENTRAL_1.getName(), 
paginationOptions(pageNumber(2)));
+      assertTrue(isEmpty(tags));
+      assertEquals(server.getRequestCount(), 1);
+      assertSent(server, "GET", "DescribeTags", ImmutableMap.of("RegionId", 
Regions.EU_CENTRAL_1.getName()), 2);
+   }
+
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/internal/BaseECSComputeServiceApiMockTest.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/internal/BaseECSComputeServiceApiMockTest.java
 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/internal/BaseECSComputeServiceApiMockTest.java
index 1b27556..dd3938d 100644
--- 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/internal/BaseECSComputeServiceApiMockTest.java
+++ 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/compute/internal/BaseECSComputeServiceApiMockTest.java
@@ -89,6 +89,10 @@ public class BaseECSComputeServiceApiMockTest {
       return new MockResponse().setStatus("HTTP/1.1 404 Not Found");
    }
 
+   protected MockResponse response204() {
+      return new MockResponse().setStatus("HTTP/1.1 204 No Content");
+   }
+
    protected String stringFromResource(String resourceName) {
       try {
          return Resources.toString(getClass().getResource(resourceName), 
Charsets.UTF_8)
@@ -98,10 +102,19 @@ public class BaseECSComputeServiceApiMockTest {
       }
    }
 
+   protected <T> T objectFromResource(String resourceName, Class<T> type) {
+      String text = stringFromResource(resourceName);
+      return json.fromJson(text, type);
+   }
+
    protected RecordedRequest assertSent(MockWebServer server, String method, 
String action) throws InterruptedException {
       return assertSent(server, method, action, ImmutableMap.<String, 
String>of(), null);
    }
 
+   protected RecordedRequest assertSent(MockWebServer server, String method, 
String action, Integer page) throws InterruptedException {
+      return assertSent(server, method, action, ImmutableMap.<String, 
String>of(), page);
+   }
+
    protected RecordedRequest assertSent(MockWebServer server, String method, 
String action, Map<String, String> queryParameters) throws InterruptedException 
{
       return assertSent(server, method, action, queryParameters, null);
    }

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/functions/ArrayToCommaSeparatedStringTest.java
----------------------------------------------------------------------
diff --git 
a/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/functions/ArrayToCommaSeparatedStringTest.java
 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/functions/ArrayToCommaSeparatedStringTest.java
new file mode 100644
index 0000000..625606f
--- /dev/null
+++ 
b/aliyun-ecs/src/test/java/org/jclouds/aliyun/ecs/functions/ArrayToCommaSeparatedStringTest.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.aliyun.ecs.functions;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+@Test(groups = "unit")
+public class ArrayToCommaSeparatedStringTest {
+
+   public void testArrayOfString() {
+      String[] input = {"Cheese", "Pepperoni", "Black Olives"};
+      String actual = new ArrayToCommaSeparatedString().apply(input);
+      assertNotNull(actual);
+      assertEquals(actual, "[\"Cheese\",\"Pepperoni\",\"Black Olives\"]");
+   }
+
+   public void testSingleArrayOfString() {
+      String[] input = {"Sun"};
+      String actual = new ArrayToCommaSeparatedString().apply(input);
+      assertNotNull(actual);
+      assertEquals(actual, "[\"Sun\"]");
+   }
+
+   public void testEmptyArrayOfString() {
+      String[] input = {};
+      String actual = new ArrayToCommaSeparatedString().apply(input);
+      assertNotNull(actual);
+      assertEquals(actual, "[]");
+   }
+
+   @Test(expectedExceptions = NullPointerException.class)
+   public void testNullInput() {
+      new ArrayToCommaSeparatedString().apply(null);
+   }
+
+   @Test(expectedExceptions = IllegalArgumentException.class, 
expectedExceptionsMessageRegExp = "This function is only valid for array of 
Strings!")
+   public void testWrongInputType() {
+      new ArrayToCommaSeparatedString().apply("wrong");
+   }
+}

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/resources/keypair-create-res.json
----------------------------------------------------------------------
diff --git a/aliyun-ecs/src/test/resources/keypair-create-res.json 
b/aliyun-ecs/src/test/resources/keypair-create-res.json
new file mode 100644
index 0000000..f302b9d
--- /dev/null
+++ b/aliyun-ecs/src/test/resources/keypair-create-res.json
@@ -0,0 +1,6 @@
+{
+  "RequestId": "022B3F72-4403-494D-A015-219236A5432E",
+  "KeyPairFingerPrint": "e70b8c018d46a81dd881adddc34e8ff5",
+  "PrivateKeyBody": "-----BEGIN RSA PRIVATE 
KEY-----\nMIIEowIBAAKCAQEAlU7ybrJx19O/DBP5iZE+n2ffCnDeaiI0A5DoinINPEyfyQdG\nie4Jki9iQQ/3WY05QyKlgpjFZt9ZJ0FbLF0yoExBuePVdCskN2jv0rZWE/6WR3ku\nOEuu1w3wfkcGJiN32cxrM7D455XAYd0c/KrRgajybloidQxFwPx/FmxZ7QNH66ww\nRoJndr0FUGldbusXmX4UO5d1i8vC0V6lOI+Jmq+7n92SV5lZb7rsjLwh3pnKIGi4\nF9Drt/HZzbdr0nLGyArO5KQs+xRzgj6a8VUOYyHpzkAd5gASwPZI8K5vSjMqRgee\nc6pBAGWzmQJ53n40gSs71FGHUmfAFOJdMZiE9QIDAQABAoIBADEIy1+FZRPfa4e4\n66O9Opa5Uyuno7OxZemh5mzJRgV+mJ85r3XO4f/LZfY+Gxqi4aJlt3trVrEROsNE\nmH+6X8z7Hj7BTzGmlW9JHDHURfKtEoeIiaBdYp8n6cpe4usVjN/PXYmNXkEYEiVR\nq5pjMwjlBjEtktFj5Wiaw9YGYYYPoInJdGizSjCgsnLErvCIdFgiO9QTCLRNIQsn\n4TUlIfZQGbv6w4IbtR5z6qGEbM6aYrbOyJtpmDA9972dUk1vNlR+ChUmOBVJ3ZPP\nUwyasRqTgGX3vjiKLGNzoHPT4HDewzyVoK/bzA+qyew/QZ5dE0sMHvf3E6lNuVd0\nP/ZL5uUCgYEA+W3+p9J2fNR39eKC7ckqpo+xyeluXoZ0wGE86oV4Q4SxSwE+nHYF\nC5TxhYzjCLKC6TUUlUBobxOdI873k03tTH3wMC7TVGs3TxNG70Yf8lv7Be5Tlcgf\nfgeLnUuUtzY1UASFpDjgpJxgkN/tb1vnaDfd6JNIQeHDOSaURBRV+xcCgYEAmT3L\nPhauJMAK3FyatXnef/J
 
Y+dpHrDTcAcfLFlUlZBb+BedG6dV03WqWe2ef7kHmsPKK\n5G4R/Bc9SpVFOpm/G6+kDRxE8WcsmPcfRKpXnkFCkYBdJO1bHcxRBAaU88Q25WiY\nBrNv5LzoWDL47LUnuL4cTw3qB4y4bi1EJklgl9MCgYEApHstb9uwuPafOK0rK8T5\ndCbT1dMyLfE6clZtBjYHrXaGN3DVqfWFtDJ+5lOWr3iQLVsMfLOhaoYjnKZxylic\nAFIYHp3yS/v72BBdOZIjpP2U1j9oLSBv6/rrzUk3A24iz+Z7fmTndoWMhFy2RTX0\nrlwQ4Lqm7pMC2uAe65oBbPcCgYBzw5fXZsDVqHJL+HUzZUZt98G5tmlwsVoGyk0k\nqNwfWbM6+HW8znGDlzLpNOY/0m8Y+5Frca+Kdm/p+QwccetKWgyfjtySVXP+dqmb\ncOfR+ND2JDe5Xsn3n9MQLHy4DmG+Op6maUW9UexgPNmJ0GyahpvSKNvEKk1lhjK2\njbY32wKBgHq90kddHF11d6efjgLmeKE6SETLYG8zwciHWc+J/LK0WizO5MRGzWKN\nExUAenjdmaVtedeYWxlMZAcNZPuzjtVZGs+VW1pRDvdJlPZx1Rl3REo5aVx1LX7I\noK2+klsagwU7TCvGHYC/1vyQrbiym8L5dfjG6J3QeAi2ZbTzYUOw\n-----END
 RSA PRIVATE KEY-----\n",
+  "KeyPairName": "jclouds-test"
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/resources/keypair-delete-res.json
----------------------------------------------------------------------
diff --git a/aliyun-ecs/src/test/resources/keypair-delete-res.json 
b/aliyun-ecs/src/test/resources/keypair-delete-res.json
new file mode 100644
index 0000000..a4fb26b
--- /dev/null
+++ b/aliyun-ecs/src/test/resources/keypair-delete-res.json
@@ -0,0 +1,3 @@
+{
+  "RequestId": "A2B4BEF6-5607-4A6E-B5B4-87A4E148E4F0"
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/resources/keypair-import-res.json
----------------------------------------------------------------------
diff --git a/aliyun-ecs/src/test/resources/keypair-import-res.json 
b/aliyun-ecs/src/test/resources/keypair-import-res.json
new file mode 100644
index 0000000..cf647db
--- /dev/null
+++ b/aliyun-ecs/src/test/resources/keypair-import-res.json
@@ -0,0 +1,6 @@
+{
+  "RequestId": "E5AE0C6B-A56E-4AD8-8110-D870FF46E96F",
+  "KeyPairFingerPrint": "9a1fc59d7d1c51a16d541ee475b7d7bb",
+  "PrivateKeyBody": "-----BEGIN RSA PRIVATE 
KEY-----\nMIIEowIBAAKCAQEAkCMCNi52fH2ZEIbei35FtT4RXiLXQ9xBLA7pYVuk35G5YPSn\nzltQ52KlugU/FRI7jXLNfrw4Kt8LIt2LlaflqRWkJjOREozFHxIhjWCMM8hOV3NK\nF3vG85vhuOF6QGBSYHDCGFJ6y+KMlfVd4O2Qm/2PgqoOYOy80W8QF6ZsSAeS93Tz\nGNyb9pzcsxv2O3rRgfoqMCF5+/0T/mDLSfIiu6WRoWEeEXHRrqcOidR2gfBEkIKE\nKljcTtPaZ17u//QKVF0akhP464w+HV6tuEgAx7FOfjgqcWG4dKh77p6uCxHVxPK7\nA2bFlSLdxtBQ6SsSTOYhMx48FtQppZ1ZRstj8wIDAQABAoIBADiXxlKHw7X1ipfW\nnKKgnbYf/Km9fFAEtwIZiMDVPtMZYHQVG54GdKmlLfTwAmi/k+ph3RWZyWPr12+F\nFT1Zgu70tFLbhGaIJw2gDNR5yBK83yWu+rRlwSP9XI8+2MVWDIIZQ9xQ5i4Pcauf\nf5DFNjZJRIPFSYf869Y/iU3/5hwRZTr4HU+YQvF/tVtdstwGfMOwNrfYdouc6uVK\nQ7bbVZsdwSSZxM3kwsK9mtZTbWEi7KpbM1mO3tZuhdrvDs7Dh33sqK9PxoN83UdB\nCupi/12sBZFkNSLzhQUTSQAMPNfypUJBjAy8y6y0kmD/eztiMKMO6uk4dPZzK1jQ\nfPb6uMECgYEA2ZAY3RqTozIH51sgqan/xD+XGbj6qVD/0WQN/pBxGooDrXkRBgO9\nRb5NTcmKBBou0TNaPEWmGA0N/tbJgnUsg2jMG8LSWii+eHM0XNtimc3eGz6Bn4Ff\n09KPJQs30rhmqUy/7M+UVE8yCk5BaCWCuBPzy6TOuxVx7eJmGUusMTkCgYEAqZoB\nT6irt5s1bCPwfhh9tc7
 
46FoHq/nykfi0hphTGqyQniKNrq4kcXDDylel0ZbAFGaX\nDXePERZ2bKRkdYSCJGnfpMtFs2v6S35T/854qleCEO6DlIlUjBV2CmjIGztGPfhj\nsLtGcLN+TkMYZeP09Q03cC1gb06+Vy5st2kT+osCgYAlxzPKEPdZ+zIMJnLBg1d6\nSGCAgvJjvEDvpyQW9BXvuc9xq/gcx0Fyft0FiN2CYNmIUhZ1KNLykjG/8qQDFz2n\ng+cNWwMTzMdmOvr4tM+mTW0n5e60N87gBUv97ri+ym5pL36ULGdhTG8wAu6wmvLb\n6/sFfZS4P70Mxadc9RrtYQKBgFHnVpTCjtKnOKBVptEuQJ8pKZkDyUqq9RK7OWr6\nar+p8Fj9tNBTtrO10keIFkLl+zKe7HmLcGK/J0eGCCGccUDmhCNQKwPftEr64dPa\nQPl6Mwy8Mnzr6RGRV6TlPyWvdVd9+Z6igfzxIaDn1AN4l5Yz4L7imvyF2XO+rq/Q\nJd7LAoGBAMy9S/NT+gLRveAAp8tFKoZ1yYC+s+6j0ztCuEuzkDioeL+YX/9nMrD7\neWg68NuQwSxBC+yTSj4E1WJJMiHehT6Slh9cgg82Eg1jgoR/eOVns0xktCYEsOu0\nV1tVN4jMAUwClxTCwWt5znKW1XVz5I80v2A67Z+0iCvGtYChiXQw\n-----END
 RSA PRIVATE KEY-----\n",
+  "KeyPairName": "jclouds-test-12345"
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/resources/keypairs-first.json
----------------------------------------------------------------------
diff --git a/aliyun-ecs/src/test/resources/keypairs-first.json 
b/aliyun-ecs/src/test/resources/keypairs-first.json
new file mode 100644
index 0000000..f88c1a8
--- /dev/null
+++ b/aliyun-ecs/src/test/resources/keypairs-first.json
@@ -0,0 +1,50 @@
+{
+  "PageNumber": 1,
+  "TotalCount": 12,
+  "KeyPairs": {
+    "KeyPair": [
+      {
+        "KeyPairFingerPrint": "188a7d0fdbfbf42a632ed34b1f7f0b12",
+        "KeyPairName": "jclouds-jclouds-imported-831"
+      },
+      {
+        "KeyPairFingerPrint": "259eea89c50e16a54ccc9558a15ca8b3",
+        "KeyPairName": "jclouds-test1"
+      },
+      {
+        "KeyPairFingerPrint": "388a7d0fdbfbf42a632ed34b1f7f0b12",
+        "KeyPairName": "jclouds-jclouds-imported-832"
+      },
+      {
+        "KeyPairFingerPrint": "459eea89c50e16a54ccc9558a15ca8b3",
+        "KeyPairName": "jclouds-test2"
+      },
+      {
+        "KeyPairFingerPrint": "588a7d0fdbfbf42a632ed34b1f7f0b12",
+        "KeyPairName": "jclouds-jclouds-imported-833"
+      },
+      {
+        "KeyPairFingerPrint": "659eea89c50e16a54ccc9558a15ca8b3",
+        "KeyPairName": "jclouds-test2"
+      },
+      {
+        "KeyPairFingerPrint": "788a7d0fdbfbf42a632ed34b1f7f0b12",
+        "KeyPairName": "jclouds-jclouds-imported-834"
+      },
+      {
+        "KeyPairFingerPrint": "859eea89c50e16a54ccc9558a15ca8b3",
+        "KeyPairName": "jclouds-test3"
+      },
+      {
+        "KeyPairFingerPrint": "988a7d0fdbfbf42a632ed34b1f7f0b12",
+        "KeyPairName": "jclouds-jclouds-imported-835"
+      },
+      {
+        "KeyPairFingerPrint": "059eea89c50e16a54ccc9558a15ca8b3",
+        "KeyPairName": "jclouds-test4"
+      }
+    ]
+  },
+  "PageSize": 10,
+  "RequestId": "3E2D8822-5058-4E2F-9D51-7A0E4EC93E33"
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/resources/keypairs-last.json
----------------------------------------------------------------------
diff --git a/aliyun-ecs/src/test/resources/keypairs-last.json 
b/aliyun-ecs/src/test/resources/keypairs-last.json
new file mode 100644
index 0000000..56685b1
--- /dev/null
+++ b/aliyun-ecs/src/test/resources/keypairs-last.json
@@ -0,0 +1,18 @@
+{
+  "PageNumber": 2,
+  "TotalCount": 12,
+  "KeyPairs": {
+    "KeyPair": [
+      {
+        "KeyPairFingerPrint": "128a7d0fdbfbf42a632ed34b1f7f0b12",
+        "KeyPairName": "jclouds-jclouds-imported-836"
+      },
+      {
+        "KeyPairFingerPrint": "059eea89c50e16a54ccc9558a15ca8b3",
+        "KeyPairName": "jclouds-test5"
+      }
+    ]
+  },
+  "PageSize": 10,
+  "RequestId": "3E2D8822-5058-4E2F-9D51-7A0E4EC93E32"
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/resources/securitygroups-first.json
----------------------------------------------------------------------
diff --git a/aliyun-ecs/src/test/resources/securitygroups-first.json 
b/aliyun-ecs/src/test/resources/securitygroups-first.json
new file mode 100644
index 0000000..fa86c32
--- /dev/null
+++ b/aliyun-ecs/src/test/resources/securitygroups-first.json
@@ -0,0 +1,61 @@
+{
+  "PageNumber": 1,
+  "TotalCount": 7,
+  "PageSize": 5,
+  "RegionId": "eu-central-1",
+  "RequestId": "3F76B1CB-6930-4D5F-81E4-70F85FC239CA",
+  "SecurityGroups": {
+    "SecurityGroup": [
+      {
+        "CreationTime": "2018-07-05T12:14:19Z",
+        "Tags": {
+          "Tag": []
+        },
+        "SecurityGroupId": "sg-gw8e5qdwvppsinxlmeu9",
+        "Description": "",
+        "SecurityGroupName": "jclouds-test",
+        "VpcId": "vpc-gw8vbir5z9j9lncyrjjhx"
+      },
+      {
+        "CreationTime": "2018-09-05T12:14:21Z",
+        "Tags": {
+          "Tag": []
+        },
+        "SecurityGroupId": "sg-gw8e5qdwvppsinxlmeu2",
+        "Description": "",
+        "SecurityGroupName": "jclouds-test2",
+        "VpcId": "vpc-gw8vbir5z9j9lncyrjjhb"
+      },
+      {
+        "CreationTime": "2018-07-03T14:34:56Z",
+        "Tags": {
+          "Tag": []
+        },
+        "SecurityGroupId": "sg-gw8izkhfxvoemvocgdsj",
+        "Description": "",
+        "SecurityGroupName": "default",
+        "VpcId": "vpc-gw8vbir5z9j9lncyrjjhx"
+      },
+      {
+        "CreationTime": "2018-07-02T07:04:03Z",
+        "Tags": {
+          "Tag": []
+        },
+        "SecurityGroupId": "sg-gw838ply3g09998rmie1",
+        "Description": "",
+        "SecurityGroupName": "jclouds-aliyun-ecs",
+        "VpcId": "vpc-gw8vbir5z9j9lncyrjjhx"
+      },
+      {
+        "CreationTime": "2018-07-02T06:49:48Z",
+        "Tags": {
+          "Tag": []
+        },
+        "SecurityGroupId": "sg-gw838ply3g0993boacgo",
+        "Description": "",
+        "SecurityGroupName": "jclouds-aliyun-ecss",
+        "VpcId": "vpc-gw8vbir5z9j9lncyrjjhx"
+      }
+    ]
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/resources/securitygroups-last.json
----------------------------------------------------------------------
diff --git a/aliyun-ecs/src/test/resources/securitygroups-last.json 
b/aliyun-ecs/src/test/resources/securitygroups-last.json
new file mode 100644
index 0000000..5e6a125
--- /dev/null
+++ b/aliyun-ecs/src/test/resources/securitygroups-last.json
@@ -0,0 +1,31 @@
+{
+  "PageNumber": 2,
+  "TotalCount": 7,
+  "PageSize": 5,
+  "RegionId": "eu-central-1",
+  "RequestId": "3F76B1CB-6930-4D5F-81E4-70F85FC239CB",
+  "SecurityGroups": {
+    "SecurityGroup": [
+      {
+        "CreationTime": "2018-07-06T12:14:21Z",
+        "Tags": {
+          "Tag": []
+        },
+        "SecurityGroupId": "sg-gw8e5qdwvppsinxlmeu6",
+        "Description": "",
+        "SecurityGroupName": "jclouds-test6",
+        "VpcId": "vpc-gw8vbir5z9j9lncyrjjhx"
+      },
+      {
+        "CreationTime": "2018-08-07T12:14:21Z",
+        "Tags": {
+          "Tag": []
+        },
+        "SecurityGroupId": "sg-gw8e5qdwvppsinxlmeu7",
+        "Description": "",
+        "SecurityGroupName": "jclouds-test7",
+        "VpcId": "vpc-gw8vbir5z9j9lncyrjjha"
+      }
+    ]
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/resources/tags-first.json
----------------------------------------------------------------------
diff --git a/aliyun-ecs/src/test/resources/tags-first.json 
b/aliyun-ecs/src/test/resources/tags-first.json
new file mode 100644
index 0000000..d694c29
--- /dev/null
+++ b/aliyun-ecs/src/test/resources/tags-first.json
@@ -0,0 +1,66 @@
+{
+  "PageNumber": 1,
+  "Tags": {
+    "Tag": [
+      {
+        "ResourceTypeCount": {
+          "Securitygroup": 1
+        },
+        "TagValue": "val1",
+        "TagKey": "key1"
+      },
+      {
+        "ResourceTypeCount": {
+          "Securitygroup": 1
+        },
+        "TagValue": "val2",
+        "TagKey": "key2"
+      },
+      {
+        "ResourceTypeCount": {
+          "Securitygroup": 1
+        },
+        "TagValue": "val3",
+        "TagKey": "key3"
+      },
+      {
+        "ResourceTypeCount": {
+          "Securitygroup": 1
+        },
+        "TagValue": "val4",
+        "TagKey": "key4"
+      },
+      {
+        "ResourceTypeCount": {
+          "Securitygroup": 1
+        },
+        "TagValue": "val5",
+        "TagKey": "key5"
+      },
+      {
+        "ResourceTypeCount": {
+          "Securitygroup": 1
+        },
+        "TagValue": "val6",
+        "TagKey": "key6"
+      },
+      {
+        "ResourceTypeCount": {
+          "Securitygroup": 1
+        },
+        "TagValue": "val7",
+        "TagKey": "key7"
+      },
+      {
+        "ResourceTypeCount": {
+          "Securitygroup": 1
+        },
+        "TagValue": "val8",
+        "TagKey": "key8"
+      }
+    ]
+  },
+  "TotalCount": 10,
+  "PageSize": 8,
+  "RequestId": "3A4B8581-8AAE-4E09-A4BB-3F568AB4293B"
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/jclouds-labs/blob/a5dbf006/aliyun-ecs/src/test/resources/tags-last.json
----------------------------------------------------------------------
diff --git a/aliyun-ecs/src/test/resources/tags-last.json 
b/aliyun-ecs/src/test/resources/tags-last.json
new file mode 100644
index 0000000..0e092c4
--- /dev/null
+++ b/aliyun-ecs/src/test/resources/tags-last.json
@@ -0,0 +1,24 @@
+{
+  "PageNumber": 2,
+  "Tags": {
+    "Tag": [
+      {
+        "ResourceTypeCount": {
+          "Securitygroup": 1
+        },
+        "TagValue": "jclouds",
+        "TagKey": "owner"
+      },
+      {
+        "ResourceTypeCount": {
+          "Securitygroup": 1
+        },
+        "TagValue": "test1",
+        "TagKey": "test1"
+      }
+    ]
+  },
+  "TotalCount": 10,
+  "PageSize": 8,
+  "RequestId": "56E7EF73-A13F-44D4-9839-0C451E999226"
+}
\ No newline at end of file

Reply via email to