You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2016/02/18 16:47:40 UTC

[22/34] brooklyn-server git commit: REST API optional Jersey compatibility

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/resources/UsageResourceTest.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/resources/UsageResourceTest.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/resources/UsageResourceTest.java
new file mode 100644
index 0000000..72392fe
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/resources/UsageResourceTest.java
@@ -0,0 +1,443 @@
+/*
+ * 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.apache.brooklyn.rest.resources;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import javax.ws.rs.core.Response;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.location.Location;
+import org.apache.brooklyn.api.location.LocationSpec;
+import org.apache.brooklyn.api.location.NoMachinesAvailableException;
+import org.apache.brooklyn.core.mgmt.internal.LocalUsageManager;
+import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.entity.software.base.SoftwareProcessEntityTest;
+import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.rest.domain.ApplicationSpec;
+import org.apache.brooklyn.rest.domain.Status;
+import org.apache.brooklyn.rest.domain.TaskSummary;
+import org.apache.brooklyn.rest.domain.UsageStatistic;
+import org.apache.brooklyn.rest.domain.UsageStatistics;
+import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest;
+import org.apache.brooklyn.rest.testing.mocks.RestMockSimpleEntity;
+import org.apache.brooklyn.util.repeat.Repeater;
+import org.apache.brooklyn.util.time.Time;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.GenericType;
+
+public class UsageResourceTest extends BrooklynRestResourceTest {
+
+    @SuppressWarnings("unused")
+    private static final Logger LOG = LoggerFactory.getLogger(UsageResourceTest.class);
+
+    private static final long TIMEOUT_MS = 10*1000;
+    
+    private Calendar testStartTime;
+    
+    private final ApplicationSpec simpleSpec = ApplicationSpec.builder().name("simple-app").
+            entities(ImmutableSet.of(new org.apache.brooklyn.rest.domain.EntitySpec("simple-ent", RestMockSimpleEntity.class.getName()))).
+            locations(ImmutableSet.of("localhost")).
+            build();
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUpMethod() {
+        ((ManagementContextInternal)getManagementContext()).getStorage().remove(LocalUsageManager.APPLICATION_USAGE_KEY);
+        ((ManagementContextInternal)getManagementContext()).getStorage().remove(LocalUsageManager.LOCATION_USAGE_KEY);
+        testStartTime = new GregorianCalendar();
+    }
+
+    @Test
+    public void testListApplicationUsages() throws Exception {
+        // Create an app
+        Calendar preStart = new GregorianCalendar();
+        String appId = createApp(simpleSpec);
+        Calendar postStart = new GregorianCalendar();
+        
+        // We will retrieve usage from one millisecond after start; this guarantees to not be  
+        // told about both STARTING+RUNNING, which could otherwise happen if they are in the 
+        // same milliscond.
+        Calendar afterPostStart = Time.newCalendarFromMillisSinceEpochUtc(postStart.getTime().getTime()+1);
+        
+        // Check that app's usage is returned
+        ClientResponse response = client().resource("/v1/usage/applications").get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        Iterable<UsageStatistics> usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
+        UsageStatistics usage = Iterables.getOnlyElement(usages);
+        assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING), roundDown(preStart), postStart);
+
+        // check app ignored if endCalendar before app started
+        response = client().resource("/v1/usage/applications?start="+0+"&end="+(preStart.getTime().getTime()-1)).get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
+        assertTrue(Iterables.isEmpty(usages), "usages="+usages);
+        
+        // Wait, so that definitely asking about things that have happened (not things in the future, 
+        // or events that are happening this exact same millisecond)
+        waitForFuture(afterPostStart.getTime().getTime());
+
+        // Check app start + end date truncated, even if running for longer (i.e. only tell us about this time window).
+        // Note that start==end means we get a snapshot of the apps in use at that exact time.
+        //
+        // The start/end times in UsageStatistic are in String format, and are rounded down to the nearest second.
+        // The comparison does use the milliseconds passed in the REST call though.
+        // The rounding down result should be the same as roundDown(afterPostStart), because that is the time-window
+        // we asked for.
+        response = client().resource("/v1/usage/applications?start="+afterPostStart.getTime().getTime()+"&end="+afterPostStart.getTime().getTime()).get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
+        usage = Iterables.getOnlyElement(usages);
+        assertAppUsage(usage, appId, ImmutableList.of(Status.RUNNING), roundDown(preStart), postStart);
+        assertAppUsageTimesTruncated(usage, roundDown(afterPostStart), roundDown(afterPostStart));
+
+        // Delete the app
+        Calendar preDelete = new GregorianCalendar();
+        deleteApp(appId);
+        Calendar postDelete = new GregorianCalendar();
+
+        // Deleted app still returned, if in time range
+        response = client().resource("/v1/usage/applications").get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
+        usage = Iterables.getOnlyElement(usages);
+        assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING, Status.DESTROYED), roundDown(preStart), postDelete);
+        assertAppUsage(ImmutableList.copyOf(usage.getStatistics()).subList(2, 3), appId, ImmutableList.of(Status.DESTROYED), roundDown(preDelete), postDelete);
+
+        long afterPostDelete = postDelete.getTime().getTime()+1;
+        waitForFuture(afterPostDelete);
+        
+        response = client().resource("/v1/usage/applications?start=" + afterPostDelete).get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
+        assertTrue(Iterables.isEmpty(usages), "usages="+usages);
+    }
+
+    @Test
+    public void testGetApplicationUsagesForNonExistantApp() throws Exception {
+        ClientResponse response = client().resource("/v1/usage/applications/wrongid").get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.NOT_FOUND.getStatusCode());
+    }
+    
+    @Test
+    public void testGetApplicationUsage() throws Exception {
+        // Create an app
+        Calendar preStart = new GregorianCalendar();
+        String appId = createApp(simpleSpec);
+        Calendar postStart = new GregorianCalendar();
+        
+        // Normal request returns all
+        ClientResponse response = client().resource("/v1/usage/applications/" + appId).get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        UsageStatistics usage = response.getEntity(new GenericType<UsageStatistics>() {});
+        assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING), roundDown(preStart), postStart);
+
+        // Time-constrained requests
+        response = client().resource("/v1/usage/applications/" + appId + "?start=1970-01-01T00:00:00-0100").get(ClientResponse.class);
+        usage = response.getEntity(new GenericType<UsageStatistics>() {});
+        assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING), roundDown(preStart), postStart);
+        
+        response = client().resource("/v1/usage/applications/" + appId + "?start=9999-01-01T00:00:00+0100").get(ClientResponse.class);
+        assertTrue(response.getStatus() >= 400, "end defaults to NOW, so future start should fail, instead got code "+response.getStatus());
+        
+        response = client().resource("/v1/usage/applications/" + appId + "?start=9999-01-01T00:00:00%2B0100&end=9999-01-02T00:00:00%2B0100").get(ClientResponse.class);
+        usage = response.getEntity(new GenericType<UsageStatistics>() {});
+        assertTrue(usage.getStatistics().isEmpty());
+
+        response = client().resource("/v1/usage/applications/" + appId + "?end=9999-01-01T00:00:00+0100").get(ClientResponse.class);
+        usage = response.getEntity(new GenericType<UsageStatistics>() {});
+        assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING), roundDown(preStart), postStart);
+
+        response = client().resource("/v1/usage/applications/" + appId + "?start=9999-01-01T00:00:00+0100&end=9999-02-01T00:00:00+0100").get(ClientResponse.class);
+        usage = response.getEntity(new GenericType<UsageStatistics>() {});
+        assertTrue(usage.getStatistics().isEmpty());
+
+        response = client().resource("/v1/usage/applications/" + appId + "?start=1970-01-01T00:00:00-0100&end=9999-01-01T00:00:00+0100").get(ClientResponse.class);
+        usage = response.getEntity(new GenericType<UsageStatistics>() {});
+        assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING), roundDown(preStart), postStart);
+        
+        response = client().resource("/v1/usage/applications/" + appId + "?end=1970-01-01T00:00:00-0100").get(ClientResponse.class);
+        usage = response.getEntity(new GenericType<UsageStatistics>() {});
+        assertTrue(usage.getStatistics().isEmpty());
+        
+        // Delete the app
+        Calendar preDelete = new GregorianCalendar();
+        deleteApp(appId);
+        Calendar postDelete = new GregorianCalendar();
+
+        // Deleted app still returned, if in time range
+        response = client().resource("/v1/usage/applications/" + appId).get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usage = response.getEntity(new GenericType<UsageStatistics>() {});
+        assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING, Status.DESTROYED), roundDown(preStart), postDelete);
+        assertAppUsage(ImmutableList.copyOf(usage.getStatistics()).subList(2, 3), appId, ImmutableList.of(Status.DESTROYED), roundDown(preDelete), postDelete);
+
+        // Deleted app not returned if terminated before time range begins
+        long afterPostDelete = postDelete.getTime().getTime()+1;
+        waitForFuture(afterPostDelete);
+        response = client().resource("/v1/usage/applications/" + appId +"?start=" + afterPostDelete).get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usage = response.getEntity(new GenericType<UsageStatistics>() {});
+        assertTrue(usage.getStatistics().isEmpty(), "usages="+usage);
+    }
+
+    @Test
+    public void testGetMachineUsagesForNonExistantMachine() throws Exception {
+        ClientResponse response = client().resource("/v1/usage/machines/wrongid").get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.NOT_FOUND.getStatusCode());
+    }
+
+    @Test
+    public void testGetMachineUsagesInitiallyEmpty() throws Exception {
+        // All machines: empty
+        ClientResponse response = client().resource("/v1/usage/machines").get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        Iterable<UsageStatistics> usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
+        assertTrue(Iterables.isEmpty(usages));
+        
+        // Specific machine that does not exist: get 404
+        response = client().resource("/v1/usage/machines/machineIdThatDoesNotExist").get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.NOT_FOUND.getStatusCode());
+    }
+
+    @Test
+    public void testListAndGetMachineUsage() throws Exception {
+        Location location = getManagementContext().getLocationManager().createLocation(LocationSpec.create(DynamicLocalhostMachineProvisioningLocation.class));
+        TestApplication app = getManagementContext().getEntityManager().createEntity(EntitySpec.create(TestApplication.class));
+        SoftwareProcessEntityTest.MyService entity = app.createAndManageChild(org.apache.brooklyn.api.entity.EntitySpec.create(SoftwareProcessEntityTest.MyService.class));
+        
+        Calendar preStart = new GregorianCalendar();
+        app.start(ImmutableList.of(location));
+        Calendar postStart = new GregorianCalendar();
+        Location machine = Iterables.getOnlyElement(entity.getLocations());
+
+        // All machines
+        ClientResponse response = client().resource("/v1/usage/machines").get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        Iterable<UsageStatistics> usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
+        UsageStatistics usage = Iterables.getOnlyElement(usages);
+        assertMachineUsage(usage, app.getId(), machine.getId(), ImmutableList.of(Status.ACCEPTED), roundDown(preStart), postStart);
+
+        // Specific machine
+        response = client().resource("/v1/usage/machines/"+machine.getId()).get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usage = response.getEntity(new GenericType<UsageStatistics>() {});
+        assertMachineUsage(usage, app.getId(), machine.getId(), ImmutableList.of(Status.ACCEPTED), roundDown(preStart), postStart);
+    }
+
+    @Test
+    public void testListMachinesUsageForApp() throws Exception {
+        Location location = getManagementContext().getLocationManager().createLocation(LocationSpec.create(DynamicLocalhostMachineProvisioningLocation.class));
+        TestApplication app = getManagementContext().getEntityManager().createEntity(EntitySpec.create(TestApplication.class));
+        SoftwareProcessEntityTest.MyService entity = app.createAndManageChild(org.apache.brooklyn.api.entity.EntitySpec.create(SoftwareProcessEntityTest.MyService.class));
+        String appId = app.getId();
+        
+        Calendar preStart = new GregorianCalendar();
+        app.start(ImmutableList.of(location));
+        Calendar postStart = new GregorianCalendar();
+        Location machine = Iterables.getOnlyElement(entity.getLocations());
+
+        // For running machine
+        ClientResponse response = client().resource("/v1/usage/machines?application="+appId).get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        Iterable<UsageStatistics> usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
+        UsageStatistics usage = Iterables.getOnlyElement(usages);
+        assertMachineUsage(usage, app.getId(), machine.getId(), ImmutableList.of(Status.ACCEPTED), roundDown(preStart), postStart);
+        
+        // Stop the machine
+        Calendar preStop = new GregorianCalendar();
+        app.stop();
+        Calendar postStop = new GregorianCalendar();
+        
+        // Deleted machine still returned, if in time range
+        response = client().resource("/v1/usage/machines?application=" + appId).get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
+        usage = Iterables.getOnlyElement(usages);
+        assertMachineUsage(usage, app.getId(), machine.getId(), ImmutableList.of(Status.ACCEPTED, Status.DESTROYED), roundDown(preStart), postStop);
+        assertMachineUsage(ImmutableList.copyOf(usage.getStatistics()).subList(1,2), appId, machine.getId(), ImmutableList.of(Status.DESTROYED), roundDown(preStop), postStop);
+
+        // Terminated machines ignored if terminated since start-time
+        long futureTime = postStop.getTime().getTime()+1;
+        waitForFuture(futureTime);
+        response = client().resource("/v1/usage/applications?start=" + futureTime).get(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
+        assertTrue(Iterables.isEmpty(usages), "usages="+usages);
+    }
+
+    private String createApp(ApplicationSpec spec) {
+        ClientResponse response = clientDeploy(spec);
+        assertEquals(response.getStatus(), Response.Status.CREATED.getStatusCode());
+        TaskSummary createTask = response.getEntity(TaskSummary.class);
+        waitForTask(createTask.getId());
+        return createTask.getEntityId();
+    }
+    
+    private void deleteApp(String appId) {
+        ClientResponse response = client().resource("/v1/applications/"+appId)
+                .delete(ClientResponse.class);
+        assertEquals(response.getStatus(), Response.Status.ACCEPTED.getStatusCode());
+        TaskSummary deletionTask = response.getEntity(TaskSummary.class);
+        waitForTask(deletionTask.getId());
+    }
+    
+    private void assertCalendarOrders(Object context, Calendar... Calendars) {
+        if (Calendars.length <= 1) return;
+        
+        long[] times = new long[Calendars.length];
+        for (int i = 0; i < times.length; i++) {
+            times[i] = millisSinceStart(Calendars[i]);
+        }
+        String err = "context="+context+"; Calendars="+Arrays.toString(Calendars) + "; CalendarsSanitized="+Arrays.toString(times);
+        
+        Calendar Calendar = Calendars[0];
+        for (int i = 1; i < Calendars.length; i++) {
+            assertTrue(Calendar.getTime().getTime() <= Calendars[i].getTime().getTime(), err);
+        }
+    }
+    
+    private void waitForTask(final String taskId) {
+        boolean success = Repeater.create()
+                .repeat(new Runnable() { public void run() {}})
+                .until(new Callable<Boolean>() {
+                    @Override public Boolean call() {
+                        ClientResponse response = client().resource("/v1/activities/"+taskId).get(ClientResponse.class);
+                        if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()) {
+                            return true;
+                        }
+                        TaskSummary summary = response.getEntity(TaskSummary.class);
+                        return summary != null && summary.getEndTimeUtc() != null;
+                    }})
+                .every(10L, TimeUnit.MILLISECONDS)
+                .limitTimeTo(TIMEOUT_MS, TimeUnit.MILLISECONDS)
+                .run();
+        assertTrue(success, "task "+taskId+" not finished");
+    }
+
+    private long millisSinceStart(Calendar time) {
+        return time.getTime().getTime() - testStartTime.getTime().getTime();
+    }
+    
+    private Calendar roundDown(Calendar calendar) {
+        long time = calendar.getTime().getTime();
+        long timeDown = ((long)(time / 1000)) * 1000;
+        return Time.newCalendarFromMillisSinceEpochUtc(timeDown);
+    }
+    
+    @SuppressWarnings("unused")
+    private Calendar roundUp(Calendar calendar) {
+        long time = calendar.getTime().getTime();
+        long timeDown = ((long)(time / 1000)) * 1000;
+        long timeUp = (time == timeDown) ? time : timeDown + 1000;
+        return Time.newCalendarFromMillisSinceEpochUtc(timeUp);
+    }
+
+    private void assertMachineUsage(UsageStatistics usage, String appId, String machineId, List<Status> states, Calendar pre, Calendar post) throws Exception {
+        assertUsage(usage.getStatistics(), appId, machineId, states, pre, post, false);
+    }
+    
+    private void assertMachineUsage(Iterable<UsageStatistic> usages, String appId, String machineId, List<Status> states, Calendar pre, Calendar post) throws Exception {
+        assertUsage(usages, appId, machineId, states, pre, post, false);
+    }
+    
+    private void assertAppUsage(UsageStatistics usage, String appId, List<Status> states, Calendar pre, Calendar post) throws Exception {
+        assertUsage(usage.getStatistics(), appId, appId, states, pre, post, false);
+    }
+    
+    private void assertAppUsage(Iterable<UsageStatistic> usages, String appId, List<Status> states, Calendar pre, Calendar post) throws Exception {
+        assertUsage(usages, appId, appId, states, pre, post, false);
+    }
+
+    private void assertUsage(Iterable<UsageStatistic> usages, String appId, String id, List<Status> states, Calendar pre, Calendar post, boolean allowGaps) throws Exception {
+        String errMsg = "usages="+usages;
+        Calendar now = new GregorianCalendar();
+        Calendar lowerBound = pre;
+        Calendar strictStart = null;
+        
+        assertEquals(Iterables.size(usages), states.size(), errMsg);
+        for (int i = 0; i < Iterables.size(usages); i++) {
+            UsageStatistic usage = Iterables.get(usages, i);
+            Calendar usageStart = Time.parseCalendar(usage.getStart());
+            Calendar usageEnd = Time.parseCalendar(usage.getEnd());
+            assertEquals(usage.getId(), id, errMsg);
+            assertEquals(usage.getApplicationId(), appId, errMsg);
+            assertEquals(usage.getStatus(), states.get(i), errMsg);
+            assertCalendarOrders(usages, lowerBound, usageStart, post);
+            assertCalendarOrders(usages, usageEnd, now);
+            if (strictStart != null) {
+                assertEquals(usageStart, strictStart, errMsg);
+            }
+            if (!allowGaps) {
+                strictStart = usageEnd;
+            }
+            lowerBound = usageEnd;
+        }
+    }
+
+    private void assertAppUsageTimesTruncated(UsageStatistics usages, Calendar strictStart, Calendar strictEnd) throws Exception {
+        String errMsg = "strictStart="+Time.makeDateString(strictStart)+"; strictEnd="+Time.makeDateString(strictEnd)+";usages="+usages;
+        Calendar usageStart = Time.parseCalendar(Iterables.getFirst(usages.getStatistics(), null).getStart());
+        Calendar usageEnd = Time.parseCalendar(Iterables.getLast(usages.getStatistics()).getStart());
+        // time zones might be different - so must convert to date
+        assertEquals(usageStart.getTime(), strictStart.getTime(), "usageStart="+Time.makeDateString(usageStart)+";"+errMsg);
+        assertEquals(usageEnd.getTime(), strictEnd.getTime(), errMsg);
+    }
+    
+    public static class DynamicLocalhostMachineProvisioningLocation extends LocalhostMachineProvisioningLocation {
+        @Override
+        public SshMachineLocation obtain(Map<?, ?> flags) throws NoMachinesAvailableException {
+            return super.obtain(flags);
+        }
+        
+        @Override
+        public void release(SshMachineLocation machine) {
+            super.release(machine);
+            super.machines.remove(machine);
+            getManagementContext().getLocationManager().unmanage(machine);
+        }
+    }
+
+    private void waitForFuture(long futureTime) throws InterruptedException {
+        long now;
+        while ((now = System.currentTimeMillis()) < futureTime) {
+            Thread.sleep(futureTime - now);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/resources/VersionResourceTest.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/resources/VersionResourceTest.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/resources/VersionResourceTest.java
new file mode 100644
index 0000000..384feb0
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/resources/VersionResourceTest.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.apache.brooklyn.rest.resources;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import javax.ws.rs.core.Response;
+
+import org.testng.annotations.Test;
+
+import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest;
+
+import com.sun.jersey.api.client.ClientResponse;
+
+public class VersionResourceTest extends BrooklynRestResourceTest {
+
+    @Test
+    public void testGetVersion() {
+        ClientResponse response = client().resource("/v1/version")
+                .get(ClientResponse.class);
+
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        String version = response.getEntity(String.class);
+// TODO johnmccabe - 19/12/2015 :: temporarily disabled while the repo split work is ongoing,
+// must be restored when switching back to a valid brooklyn version
+//        assertTrue(version.matches("^\\d+\\.\\d+\\.\\d+.*"));
+        assertTrue(true);
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    protected void addBrooklynResources() {
+        addResource(new VersionResource());
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/security/PasswordHasherTest.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/security/PasswordHasherTest.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/security/PasswordHasherTest.java
new file mode 100644
index 0000000..575d6a4
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/security/PasswordHasherTest.java
@@ -0,0 +1,37 @@
+/*
+ * 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.apache.brooklyn.rest.security;
+
+import static org.testng.Assert.assertEquals;
+
+import org.testng.annotations.Test;
+
+public class PasswordHasherTest {
+
+    @Test
+    public void testHashSha256() throws Exception {
+        // Note: expected hash values generated externally:
+        // echo -n mysaltmypassword | openssl dgst -sha256
+
+        assertEquals(PasswordHasher.sha256("mysalt", "mypassword"), "d02878b06efa88579cd84d9e50b211c0a7caa92cf243bad1622c66081f7e2692");
+        assertEquals(PasswordHasher.sha256("", "mypassword"), "89e01536ac207279409d4de1e5253e01f4a1769e696db0d6062ca9b8f56767c8");
+        assertEquals(PasswordHasher.sha256(null, "mypassword"), "89e01536ac207279409d4de1e5253e01f4a1769e696db0d6062ca9b8f56767c8");
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/security/provider/TestSecurityProvider.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/security/provider/TestSecurityProvider.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/security/provider/TestSecurityProvider.java
new file mode 100644
index 0000000..cad251f
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/security/provider/TestSecurityProvider.java
@@ -0,0 +1,46 @@
+/*
+ * 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.apache.brooklyn.rest.security.provider;
+
+import javax.servlet.http.HttpSession;
+
+import org.apache.http.auth.UsernamePasswordCredentials;
+
+public class TestSecurityProvider implements SecurityProvider {
+
+    public static final String USER = "test";
+    public static final String PASSWORD = "opensesame";
+    public static final UsernamePasswordCredentials CREDENTIAL =
+            new UsernamePasswordCredentials(TestSecurityProvider.USER, TestSecurityProvider.PASSWORD);
+
+    @Override
+    public boolean isAuthenticated(HttpSession session) {
+        return false;
+    }
+
+    @Override
+    public boolean authenticate(HttpSession session, String user, String password) {
+        return USER.equals(user) && PASSWORD.equals(password);
+    }
+
+    @Override
+    public boolean logout(HttpSession session) {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/test/config/render/TestRendererHints.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/test/config/render/TestRendererHints.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/test/config/render/TestRendererHints.java
new file mode 100644
index 0000000..91294db
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/test/config/render/TestRendererHints.java
@@ -0,0 +1,36 @@
+/*
+ * 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.apache.brooklyn.rest.test.config.render;
+
+import org.apache.brooklyn.core.config.render.RendererHints;
+
+/** Methods used when testing the {@link RendererHints} regiostry. */
+public class TestRendererHints {
+
+    /** Clear the registry. 
+     *
+     *  MUST be used by a single test only.
+     *  TestNG interleaves the tests (sequentially) which results in tearDown 
+     *  executing in the middle of another class' tests. Only one tearDown may
+     *  call this method.
+     **/
+    public static void clearRegistry() {
+        RendererHints.getRegistry().clear();
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/test/entity/brooklynnode/DeployBlueprintTest.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/test/entity/brooklynnode/DeployBlueprintTest.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/test/entity/brooklynnode/DeployBlueprintTest.java
new file mode 100644
index 0000000..2ab62a9
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/test/entity/brooklynnode/DeployBlueprintTest.java
@@ -0,0 +1,89 @@
+/*
+ * 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.apache.brooklyn.rest.test.entity.brooklynnode;
+
+import static org.testng.Assert.assertEquals;
+
+import java.net.URI;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.api.mgmt.EntityManager;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode;
+import org.apache.brooklyn.entity.brooklynnode.BrooklynNode.DeployBlueprintEffector;
+import org.apache.brooklyn.entity.stock.BasicApplication;
+import org.apache.brooklyn.feed.http.JsonFunctions;
+import org.apache.brooklyn.rest.BrooklynRestApiLauncherTestFixture;
+import org.apache.brooklyn.test.HttpTestUtils;
+import org.apache.brooklyn.util.guava.Functionals;
+import org.eclipse.jetty.server.Server;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class DeployBlueprintTest extends BrooklynRestApiLauncherTestFixture {
+
+    private static final Logger log = LoggerFactory.getLogger(DeployBlueprintTest.class);
+
+    Server server;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        server = newServer();
+        useServerForTest(server);
+    }
+
+    @Test
+    public void testStartsAppViaEffector() throws Exception {
+        URI webConsoleUri = URI.create(getBaseUri());
+
+        EntitySpec<BrooklynNode> spec = EntitySpec.create(BrooklynNode.class);
+        EntityManager mgr = getManagementContextFromJettyServerAttributes(server).getEntityManager();
+        BrooklynNode node = mgr.createEntity(spec);
+        node.sensors().set(BrooklynNode.WEB_CONSOLE_URI, webConsoleUri);
+        mgr.manage(node);
+        Map<String, String> params = ImmutableMap.of(DeployBlueprintEffector.BLUEPRINT_CAMP_PLAN.getName(), "{ services: [ serviceType: \"java:"+BasicApplication.class.getName()+"\" ] }");
+        String id = node.invoke(BrooklynNode.DEPLOY_BLUEPRINT, params).getUnchecked();
+
+        log.info("got: "+id);
+
+        String apps = HttpTestUtils.getContent(webConsoleUri.toString()+"/v1/applications");
+        List<String> appType = parseJsonList(apps, ImmutableList.of("spec", "type"), String.class);
+        assertEquals(appType, ImmutableList.of(BasicApplication.class.getName()));
+        
+        String status = HttpTestUtils.getContent(webConsoleUri.toString()+"/v1/applications/"+id+"/entities/"+id+"/sensors/service.status");
+        log.info("STATUS: "+status);
+    }
+    
+    private <T> List<T> parseJsonList(String json, List<String> elements, Class<T> clazz) {
+        Function<String, List<T>> func = Functionals.chain(
+                JsonFunctions.asJson(),
+                JsonFunctions.forEach(Functionals.chain(
+                        JsonFunctions.walk(elements),
+                        JsonFunctions.cast(clazz))));
+        return func.apply(json);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestApiTest.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestApiTest.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestApiTest.java
new file mode 100644
index 0000000..bf3f8af
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestApiTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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.apache.brooklyn.rest.testing;
+
+import java.net.URI;
+
+import org.apache.brooklyn.api.location.LocationRegistry;
+import org.apache.brooklyn.api.mgmt.ManagementContext;
+import org.apache.brooklyn.camp.brooklyn.BrooklynCampPlatformLauncherNoServer;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.core.location.BasicLocationRegistry;
+import org.apache.brooklyn.core.mgmt.ManagementContextInjectable;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.rest.BrooklynRestApi;
+import org.apache.brooklyn.rest.BrooklynRestApiLauncherTest;
+import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils;
+import org.apache.brooklyn.rest.util.ManagementContextProvider;
+import org.apache.brooklyn.rest.util.NoOpRecordingShutdownHandler;
+import org.apache.brooklyn.rest.util.NullHttpServletRequestProvider;
+import org.apache.brooklyn.rest.util.NullServletConfigProvider;
+import org.apache.brooklyn.rest.util.ShutdownHandlerProvider;
+import org.apache.brooklyn.rest.util.json.BrooklynJacksonJsonProvider;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeMethod;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Preconditions;
+import com.sun.jersey.api.client.Client;
+import com.sun.jersey.api.client.WebResource;
+import com.sun.jersey.api.core.DefaultResourceConfig;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.LowLevelAppDescriptor;
+import com.sun.jersey.test.framework.spi.container.TestContainer;
+import com.sun.jersey.test.framework.spi.container.TestContainerException;
+import com.sun.jersey.test.framework.spi.container.TestContainerFactory;
+import com.sun.jersey.test.framework.spi.container.inmemory.InMemoryTestContainerFactory;
+
+public abstract class BrooklynRestApiTest {
+
+    protected ManagementContext manager;
+    
+    protected boolean useLocalScannedCatalog = false;
+    protected NoOpRecordingShutdownHandler shutdownListener = createShutdownHandler();
+
+    @BeforeMethod(alwaysRun = true)
+    public void setUpMethod() {
+        shutdownListener.reset();
+    }
+    
+    protected synchronized void useLocalScannedCatalog() {
+        if (manager!=null && !useLocalScannedCatalog)
+            throw new IllegalStateException("useLocalScannedCatalog must be specified before manager is accessed/created");
+        useLocalScannedCatalog = true;
+    }
+    
+    private NoOpRecordingShutdownHandler createShutdownHandler() {
+        return new NoOpRecordingShutdownHandler();
+    }
+
+    protected synchronized ManagementContext getManagementContext() {
+        if (manager==null) {
+            if (useLocalScannedCatalog) {
+                manager = new LocalManagementContext();
+                BrooklynRestApiLauncherTest.forceUseOfDefaultCatalogWithJavaClassPath(manager);
+            } else {
+                manager = new LocalManagementContextForTests();
+            }
+            manager.getHighAvailabilityManager().disabled();
+            BasicLocationRegistry.setupLocationRegistryForTesting(manager);
+            
+            new BrooklynCampPlatformLauncherNoServer()
+                .useManagementContext(manager)
+                .launch();
+        }
+        return manager;
+    }
+    
+    protected ObjectMapper mapper() {
+        return BrooklynJacksonJsonProvider.findSharedObjectMapper(null, getManagementContext());
+    }
+    
+    @AfterClass
+    public void tearDown() throws Exception {
+        destroyManagementContext();
+    }
+
+    protected void destroyManagementContext() {
+        if (manager!=null) {
+            Entities.destroyAll(manager);
+            manager = null;
+        }
+    }
+    
+    public LocationRegistry getLocationRegistry() {
+        return new BrooklynRestResourceUtils(getManagementContext()).getLocationRegistry();
+    }
+
+    private JerseyTest jerseyTest;
+    protected DefaultResourceConfig config = new DefaultResourceConfig();
+    
+    protected final void addResource(Object resource) {
+        Preconditions.checkNotNull(config, "Must run before setUpJersey");
+        
+        if (resource instanceof Class)
+            config.getClasses().add((Class<?>)resource);
+        else
+            config.getSingletons().add(resource);
+        
+        if (resource instanceof ManagementContextInjectable) {
+            ((ManagementContextInjectable)resource).setManagementContext(getManagementContext());
+        }
+    }
+    
+    protected final void addProvider(Class<?> provider) {
+        Preconditions.checkNotNull(config, "Must run before setUpJersey");
+        
+        config.getClasses().add(provider);
+    }
+    
+    protected void addDefaultResources() {
+        // seems we have to provide our own injector because the jersey test framework 
+        // doesn't inject ServletConfig and it all blows up
+        // and the servlet config provider must be an instance; addClasses doesn't work for some reason
+        addResource(new NullServletConfigProvider());
+        addResource(new ManagementContextProvider(getManagementContext()));
+        addProvider(NullHttpServletRequestProvider.class);
+        addResource(new ShutdownHandlerProvider(shutdownListener));
+    }
+
+    protected final void setUpResources() {
+        addDefaultResources();
+        addBrooklynResources();
+        for (Object r: BrooklynRestApi.getMiscResources())
+            addResource(r);
+    }
+
+    /** intended for overriding if you only want certain resources added, or additional ones added */
+    protected void addBrooklynResources() {
+        for (Object r: BrooklynRestApi.getBrooklynRestResources())
+            addResource(r);
+    }
+
+    protected void setUpJersey() {
+        setUpResources();
+        
+        jerseyTest = createJerseyTest();
+        config = null;
+        try {
+            jerseyTest.setUp();
+        } catch (Exception e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+
+    protected JerseyTest createJerseyTest() {
+        return new JerseyTest() {
+            @Override
+            protected AppDescriptor configure() {
+                return new LowLevelAppDescriptor.Builder(config).build();
+            }
+
+            @Override
+            protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
+                return new TestContainerFactory() {
+                    TestContainerFactory delegate = new InMemoryTestContainerFactory();
+                    
+                    @Override
+                    public Class<? extends AppDescriptor> supports() {
+                        return delegate.supports();
+                    }
+                    
+                    @Override
+                    public TestContainer create(URI baseUri, AppDescriptor ad)
+                            throws IllegalArgumentException {
+                        URI uri = URI.create(baseUri.toString() + "v1/");
+                        System.out.println(uri);;
+                        return delegate.create(uri, (LowLevelAppDescriptor)ad);
+                    }
+                };
+            }
+        };
+    }
+    
+    protected void tearDownJersey() {
+        if (jerseyTest != null) {
+            try {
+                jerseyTest.tearDown();
+            } catch (Exception e) {
+                throw Exceptions.propagate(e);
+            }
+        }
+        config = new DefaultResourceConfig();
+    }
+
+    public Client client() {
+        Preconditions.checkNotNull(jerseyTest, "Must run setUpJersey first");
+        return jerseyTest.client();
+    }
+
+    public WebResource resource(String uri) {
+        Preconditions.checkNotNull(jerseyTest, "Must run setUpJersey first");
+        return jerseyTest.resource().path(uri);
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestResourceTest.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestResourceTest.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestResourceTest.java
new file mode 100644
index 0000000..b94e73c
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestResourceTest.java
@@ -0,0 +1,154 @@
+/*
+ * 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.apache.brooklyn.rest.testing;
+
+import static org.testng.Assert.assertTrue;
+
+import java.net.URI;
+import java.util.Collection;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+
+import javax.ws.rs.core.MediaType;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.apache.brooklyn.api.entity.Application;
+import org.apache.brooklyn.core.entity.Entities;
+import org.apache.brooklyn.rest.domain.ApplicationSpec;
+import org.apache.brooklyn.rest.domain.ApplicationSummary;
+import org.apache.brooklyn.rest.domain.Status;
+import org.apache.brooklyn.util.exceptions.Exceptions;
+import org.apache.brooklyn.util.repeat.Repeater;
+import org.apache.brooklyn.util.time.Duration;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.spi.inject.Errors;
+
+public abstract class BrooklynRestResourceTest extends BrooklynRestApiTest {
+
+    private static final Logger log = LoggerFactory.getLogger(BrooklynRestResourceTest.class);
+    
+    @BeforeClass(alwaysRun = true)
+    public void setUp() throws Exception {
+        // need this to debug jersey inject errors
+        java.util.logging.Logger.getLogger(Errors.class.getName()).setLevel(Level.INFO);
+
+        setUpJersey();
+    }
+
+    @Override
+    @AfterClass(alwaysRun = true)
+    public void tearDown() throws Exception {
+        tearDownJersey();
+        super.tearDown();
+    }
+
+
+    protected ClientResponse clientDeploy(ApplicationSpec spec) {
+        try {
+            // dropwizard TestClient won't skip deserialization of trivial things like string and byte[] and inputstream
+            // if we pass in an object it serializes, so we have to serialize things ourselves
+            return client().resource("/v1/applications")
+                .entity(new ObjectMapper().writer().writeValueAsBytes(spec), MediaType.APPLICATION_OCTET_STREAM)
+                .post(ClientResponse.class);
+        } catch (Exception e) {
+            throw Exceptions.propagate(e);
+        }
+    }
+    
+    protected void waitForApplicationToBeRunning(final URI applicationRef) {
+        waitForApplicationToBeRunning(applicationRef, Duration.minutes(3));
+    }
+    protected void waitForApplicationToBeRunning(final URI applicationRef, Duration timeout) {
+        if (applicationRef==null)
+            throw new NullPointerException("No application URI available (consider using BrooklynRestResourceTest.clientDeploy)");
+        
+        boolean started = Repeater.create("Wait for application startup")
+                .until(new Callable<Boolean>() {
+                    @Override
+                    public Boolean call() throws Exception {
+                        Status status = getApplicationStatus(applicationRef);
+                        if (status == Status.ERROR) {
+                            Assert.fail("Application failed with ERROR");
+                        }
+                        return status == Status.RUNNING;
+                    }
+                })
+                .backoffTo(Duration.ONE_SECOND)
+                .limitTimeTo(timeout)
+                .run();
+        
+        if (!started) {
+            log.warn("Did not start application "+applicationRef+":");
+            Collection<Application> apps = getManagementContext().getApplications();
+            for (Application app: apps)
+                Entities.dumpInfo(app);
+        }
+        assertTrue(started);
+    }
+
+    protected Status getApplicationStatus(URI uri) {
+        return client().resource(uri).get(ApplicationSummary.class).getStatus();
+    }
+
+    protected void waitForPageFoundResponse(final String resource, final Class<?> clazz) {
+        boolean found = Repeater.create("Wait for page found")
+                .until(new Callable<Boolean>() {
+                    @Override
+                    public Boolean call() throws Exception {
+                        try {
+                            client().resource(resource).get(clazz);
+                            return true;
+                        } catch (UniformInterfaceException e) {
+                            return false;
+                        }
+                    }
+                })
+                .every(1, TimeUnit.SECONDS)
+                .limitTimeTo(30, TimeUnit.SECONDS)
+                .run();
+        assertTrue(found);
+    }
+    
+    protected void waitForPageNotFoundResponse(final String resource, final Class<?> clazz) {
+        boolean success = Repeater.create("Wait for page not found")
+                .until(new Callable<Boolean>() {
+                    @Override
+                    public Boolean call() throws Exception {
+                        try {
+                            client().resource(resource).get(clazz);
+                            return false;
+                        } catch (UniformInterfaceException e) {
+                            return e.getResponse().getStatus() == 404;
+                        }
+                    }
+                })
+                .every(1, TimeUnit.SECONDS)
+                .limitTimeTo(30, TimeUnit.SECONDS)
+                .run();
+        assertTrue(success);
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/CapitalizePolicy.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/CapitalizePolicy.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/CapitalizePolicy.java
new file mode 100644
index 0000000..7d80a6f
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/CapitalizePolicy.java
@@ -0,0 +1,33 @@
+/*
+ * 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.apache.brooklyn.rest.testing.mocks;
+
+import org.apache.brooklyn.api.entity.EntityLocal;
+import org.apache.brooklyn.core.policy.AbstractPolicy;
+
+@SuppressWarnings("deprecation")
+public class CapitalizePolicy extends AbstractPolicy {
+
+    @Override
+    public void setEntity(EntityLocal entity) {
+        super.setEntity(entity);
+        // TODO subscribe to foo and emit an enriched sensor on different channel which is capitalized
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroup.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroup.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroup.java
new file mode 100644
index 0000000..707c4a3
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroup.java
@@ -0,0 +1,27 @@
+/*
+ * 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.apache.brooklyn.rest.testing.mocks;
+
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.entity.ImplementedBy;
+
+@ImplementedBy(EverythingGroupImpl.class)
+public interface EverythingGroup extends Group {
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroupImpl.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroupImpl.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroupImpl.java
new file mode 100644
index 0000000..8b2c98b
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroupImpl.java
@@ -0,0 +1,32 @@
+/*
+ * 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.apache.brooklyn.rest.testing.mocks;
+
+import org.apache.brooklyn.entity.group.DynamicGroupImpl;
+
+import com.google.common.base.Predicates;
+
+public class EverythingGroupImpl extends DynamicGroupImpl implements EverythingGroup {
+
+    public EverythingGroupImpl() {
+        super();
+        config().set(ENTITY_FILTER, Predicates.alwaysTrue());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroup.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroup.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroup.java
new file mode 100644
index 0000000..f9a2e21
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroup.java
@@ -0,0 +1,30 @@
+/*
+ * 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.apache.brooklyn.rest.testing.mocks;
+
+import org.apache.brooklyn.api.entity.Group;
+import org.apache.brooklyn.api.entity.ImplementedBy;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.ConfigKeys;
+
+@ImplementedBy(NameMatcherGroupImpl.class)
+public interface NameMatcherGroup extends Group {
+
+    public static final ConfigKey<String> NAME_REGEX = ConfigKeys.newStringConfigKey("namematchergroup.regex");
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroupImpl.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroupImpl.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroupImpl.java
new file mode 100644
index 0000000..bec416f
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroupImpl.java
@@ -0,0 +1,33 @@
+/*
+ * 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.apache.brooklyn.rest.testing.mocks;
+
+import org.apache.brooklyn.core.entity.EntityPredicates;
+import org.apache.brooklyn.entity.group.DynamicGroupImpl;
+import org.apache.brooklyn.util.text.StringPredicates;
+
+public class NameMatcherGroupImpl extends DynamicGroupImpl implements NameMatcherGroup {
+
+    @Override
+    public void init() {
+        super.init();
+        config().set(ENTITY_FILTER, EntityPredicates.displayNameSatisfies(StringPredicates.matchesRegex(getConfig(NAME_REGEX))));
+        rescanEntities();
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockApp.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockApp.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockApp.java
new file mode 100644
index 0000000..6d92e65
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockApp.java
@@ -0,0 +1,24 @@
+/*
+ * 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.apache.brooklyn.rest.testing.mocks;
+
+import org.apache.brooklyn.core.entity.AbstractApplication;
+
+public class RestMockApp extends AbstractApplication {
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockAppBuilder.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockAppBuilder.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockAppBuilder.java
new file mode 100644
index 0000000..1ca10bd
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockAppBuilder.java
@@ -0,0 +1,39 @@
+/*
+ * 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.apache.brooklyn.rest.testing.mocks;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntitySpec;
+import org.apache.brooklyn.core.entity.StartableApplication;
+import org.apache.brooklyn.core.entity.factory.ApplicationBuilder;
+import org.apache.brooklyn.util.javalang.Reflections;
+
+public class RestMockAppBuilder extends ApplicationBuilder {
+
+    public RestMockAppBuilder() {
+        super(EntitySpec.create(StartableApplication.class).impl(RestMockApp.class));
+    }
+    
+    @Override
+    protected void doBuild() {
+        addChild(EntitySpec.create(Entity.class).impl(RestMockSimpleEntity.class)
+            .additionalInterfaces(Reflections.getAllInterfaces(RestMockSimpleEntity.class))
+            .displayName("child1"));
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimpleEntity.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimpleEntity.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimpleEntity.java
new file mode 100644
index 0000000..58d24aa
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimpleEntity.java
@@ -0,0 +1,103 @@
+/*
+ * 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.apache.brooklyn.rest.testing.mocks;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.entity.EntityLocal;
+import org.apache.brooklyn.api.sensor.AttributeSensor;
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.annotation.Effector;
+import org.apache.brooklyn.core.annotation.EffectorParam;
+import org.apache.brooklyn.core.config.BasicConfigKey;
+import org.apache.brooklyn.core.effector.MethodEffector;
+import org.apache.brooklyn.core.sensor.BasicAttributeSensor;
+import org.apache.brooklyn.entity.software.base.AbstractSoftwareProcessSshDriver;
+import org.apache.brooklyn.entity.software.base.SoftwareProcessImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.apache.brooklyn.location.ssh.SshMachineLocation;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+
+public class RestMockSimpleEntity extends SoftwareProcessImpl {
+
+    private static final Logger log = LoggerFactory.getLogger(RestMockSimpleEntity.class);
+    
+    public RestMockSimpleEntity() {
+        super();
+    }
+
+    public RestMockSimpleEntity(Entity parent) {
+        super(parent);
+    }
+
+    public RestMockSimpleEntity(@SuppressWarnings("rawtypes") Map flags, Entity parent) {
+        super(flags, parent);
+    }
+
+    public RestMockSimpleEntity(@SuppressWarnings("rawtypes") Map flags) {
+        super(flags);
+    }
+    
+    @Override
+    protected void connectSensors() {
+        super.connectSensors();
+        connectServiceUpIsRunning();
+    }
+
+    @SetFromFlag("sampleConfig")
+    public static final ConfigKey<String> SAMPLE_CONFIG = new BasicConfigKey<String>(
+            String.class, "brooklyn.rest.mock.sample.config", "Mock sample config", "DEFAULT_VALUE");
+
+    public static final AttributeSensor<String> SAMPLE_SENSOR = new BasicAttributeSensor<String>(
+            String.class, "brooklyn.rest.mock.sample.sensor", "Mock sample sensor");
+
+    public static final MethodEffector<String> SAMPLE_EFFECTOR = new MethodEffector<String>(RestMockSimpleEntity.class, "sampleEffector");
+    
+    @Effector
+    public String sampleEffector(@EffectorParam(name="param1", description="param one") String param1, 
+            @EffectorParam(name="param2") Integer param2) {
+        log.info("Invoked sampleEffector("+param1+","+param2+")");
+        String result = ""+param1+param2;
+        sensors().set(SAMPLE_SENSOR, result);
+        return result;
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Override
+    public Class getDriverInterface() {
+        return MockSshDriver.class;
+    }
+    
+    public static class MockSshDriver extends AbstractSoftwareProcessSshDriver {
+        public MockSshDriver(EntityLocal entity, SshMachineLocation machine) {
+            super(entity, machine);
+        }
+        public boolean isRunning() { return true; }
+        public void stop() {}
+        public void kill() {}
+        public void install() {}
+        public void customize() {}
+        public void launch() {}
+        public void setup() { }
+        public void copyInstallResources() { }
+        public void copyRuntimeResources() { }
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimplePolicy.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimplePolicy.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimplePolicy.java
new file mode 100644
index 0000000..e15cdd1
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimplePolicy.java
@@ -0,0 +1,64 @@
+/*
+ * 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.apache.brooklyn.rest.testing.mocks;
+
+import java.util.Map;
+
+import org.apache.brooklyn.config.ConfigKey;
+import org.apache.brooklyn.core.config.BasicConfigKey;
+import org.apache.brooklyn.core.policy.AbstractPolicy;
+import org.apache.brooklyn.util.core.flags.SetFromFlag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class RestMockSimplePolicy extends AbstractPolicy {
+
+    @SuppressWarnings("unused")
+    private static final Logger log = LoggerFactory.getLogger(RestMockSimplePolicy.class);
+
+    public RestMockSimplePolicy() {
+        super();
+    }
+
+    @SuppressWarnings("rawtypes")
+    public RestMockSimplePolicy(Map flags) {
+        super(flags);
+    }
+
+    @SetFromFlag("sampleConfig")
+    public static final ConfigKey<String> SAMPLE_CONFIG = BasicConfigKey.builder(String.class)
+            .name("brooklyn.rest.mock.sample.config")
+            .description("Mock sample config")
+            .defaultValue("DEFAULT_VALUE")
+            .reconfigurable(true)
+            .build();
+
+    @SetFromFlag
+    public static final ConfigKey<Integer> INTEGER_CONFIG = BasicConfigKey.builder(Integer.class)
+            .name("brooklyn.rest.mock.sample.integer")
+            .description("Mock integer config")
+            .defaultValue(1)
+            .reconfigurable(true)
+            .build();
+
+    @Override
+    protected <T> void doReconfigureConfig(ConfigKey<T> key, T val) {
+        // no-op
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/abd2d5f3/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/util/BrooklynRestResourceUtilsTest.java
----------------------------------------------------------------------
diff --git a/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/util/BrooklynRestResourceUtilsTest.java b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/util/BrooklynRestResourceUtilsTest.java
new file mode 100644
index 0000000..48908e3
--- /dev/null
+++ b/rest/rest-server-jersey/src/test/java/org/apache/brooklyn/rest/util/BrooklynRestResourceUtilsTest.java
@@ -0,0 +1,213 @@
+/*
+ * 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.apache.brooklyn.rest.util;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+import java.util.Map;
+
+import org.apache.brooklyn.api.catalog.Catalog;
+import org.apache.brooklyn.api.entity.Application;
+import org.apache.brooklyn.api.entity.Entity;
+import org.apache.brooklyn.api.policy.Policy;
+import org.apache.brooklyn.core.catalog.internal.CatalogItemBuilder;
+import org.apache.brooklyn.core.catalog.internal.CatalogTemplateItemDto;
+import org.apache.brooklyn.core.catalog.internal.CatalogUtils;
+import org.apache.brooklyn.core.entity.AbstractApplication;
+import org.apache.brooklyn.core.mgmt.internal.LocalManagementContext;
+import org.apache.brooklyn.core.objs.proxy.EntityProxy;
+import org.apache.brooklyn.core.policy.AbstractPolicy;
+import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.core.test.entity.TestApplication;
+import org.apache.brooklyn.core.test.entity.TestEntity;
+import org.apache.brooklyn.entity.stock.BasicEntity;
+import org.apache.brooklyn.rest.domain.ApplicationSpec;
+import org.apache.brooklyn.rest.domain.EntitySpec;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+
+public class BrooklynRestResourceUtilsTest {
+
+    private LocalManagementContext managementContext;
+    private BrooklynRestResourceUtils util;
+
+    @BeforeMethod(alwaysRun=true)
+    public void setUp() throws Exception {
+        managementContext = LocalManagementContextForTests.newInstance();
+        util = new BrooklynRestResourceUtils(managementContext);
+    }
+    
+    @AfterMethod(alwaysRun=true)
+    public void tearDown() throws Exception {
+        if (managementContext != null) managementContext.terminate();
+    }
+
+    @Test
+    public void testCreateAppFromImplClass() {
+        ApplicationSpec spec = ApplicationSpec.builder()
+                .name("myname")
+                .type(SampleNoOpApplication.class.getName())
+                .locations(ImmutableSet.of("localhost"))
+                .build();
+        Application app = util.create(spec);
+        
+        assertEquals(ImmutableList.copyOf(managementContext.getApplications()), ImmutableList.of(app));
+        assertEquals(app.getDisplayName(), "myname");
+        assertTrue(app instanceof EntityProxy);
+        assertTrue(app instanceof MyInterface);
+        assertFalse(app instanceof SampleNoOpApplication);
+    }
+
+    @Test
+    public void testCreateAppFromCatalogByType() {
+        createAppFromCatalog(SampleNoOpApplication.class.getName());
+    }
+
+    @Test
+    public void testCreateAppFromCatalogByName() {
+        createAppFromCatalog("app.noop");
+    }
+
+    @Test
+    public void testCreateAppFromCatalogById() {
+        createAppFromCatalog("app.noop:0.0.1");
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void testCreateAppFromCatalogByTypeMultipleItems() {
+        CatalogTemplateItemDto item = CatalogItemBuilder.newTemplate("app.noop", "0.0.2-SNAPSHOT")
+                .javaType(SampleNoOpApplication.class.getName())
+                .build();
+        managementContext.getCatalog().addItem(item);
+        createAppFromCatalog(SampleNoOpApplication.class.getName());
+    }
+
+    @SuppressWarnings("deprecation")
+    private void createAppFromCatalog(String type) {
+        CatalogTemplateItemDto item = CatalogItemBuilder.newTemplate("app.noop", "0.0.1")
+            .javaType(SampleNoOpApplication.class.getName())
+            .build();
+        managementContext.getCatalog().addItem(item);
+        
+        ApplicationSpec spec = ApplicationSpec.builder()
+                .name("myname")
+                .type(type)
+                .locations(ImmutableSet.of("localhost"))
+                .build();
+        Application app = util.create(spec);
+
+        assertEquals(app.getCatalogItemId(), "app.noop:0.0.1");
+    }
+
+    @Test
+    public void testEntityAppFromCatalogByType() {
+        createEntityFromCatalog(BasicEntity.class.getName());
+    }
+
+    @Test
+    public void testEntityAppFromCatalogByName() {
+        createEntityFromCatalog("app.basic");
+    }
+
+    @Test
+    public void testEntityAppFromCatalogById() {
+        createEntityFromCatalog("app.basic:0.0.1");
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    public void testEntityAppFromCatalogByTypeMultipleItems() {
+        CatalogTemplateItemDto item = CatalogItemBuilder.newTemplate("app.basic", "0.0.2-SNAPSHOT")
+                .javaType(SampleNoOpApplication.class.getName())
+                .build();
+        managementContext.getCatalog().addItem(item);
+        createEntityFromCatalog(BasicEntity.class.getName());
+    }
+
+    @SuppressWarnings("deprecation")
+    private void createEntityFromCatalog(String type) {
+        String symbolicName = "app.basic";
+        String version = "0.0.1";
+        CatalogTemplateItemDto item = CatalogItemBuilder.newTemplate(symbolicName, version)
+            .javaType(BasicEntity.class.getName())
+            .build();
+        managementContext.getCatalog().addItem(item);
+
+        ApplicationSpec spec = ApplicationSpec.builder()
+                .name("myname")
+                .entities(ImmutableSet.of(new EntitySpec(type)))
+                .locations(ImmutableSet.of("localhost"))
+                .build();
+        Application app = util.create(spec);
+
+        Entity entity = Iterables.getOnlyElement(app.getChildren());
+        assertEquals(entity.getCatalogItemId(), CatalogUtils.getVersionedId(symbolicName, version));
+    }
+
+    @Test
+    public void testNestedApplications() {
+        // hierarchy is: app -> subapp -> subentity (where subentity has a policy)
+        
+        Application app = managementContext.getEntityManager().createEntity(org.apache.brooklyn.api.entity.EntitySpec.create(TestApplication.class)
+                .displayName("app")
+                .child(org.apache.brooklyn.api.entity.EntitySpec.create(TestApplication.class)
+                        .displayName("subapp")
+                        .child(org.apache.brooklyn.api.entity.EntitySpec.create(TestEntity.class)
+                                .displayName("subentity")
+                                .policy(org.apache.brooklyn.api.policy.PolicySpec.create(MyPolicy.class)
+                                        .displayName("mypolicy")))));
+
+        Application subapp = (Application) Iterables.getOnlyElement(app.getChildren());
+        TestEntity subentity = (TestEntity) Iterables.getOnlyElement(subapp.getChildren());
+        
+        Entity subappRetrieved = util.getEntity(app.getId(), subapp.getId());
+        assertEquals(subappRetrieved.getDisplayName(), "subapp");
+        
+        Entity subentityRetrieved = util.getEntity(app.getId(), subentity.getId());
+        assertEquals(subentityRetrieved.getDisplayName(), "subentity");
+        
+        Policy subappPolicy = util.getPolicy(app.getId(), subentity.getId(), "mypolicy");
+        assertEquals(subappPolicy.getDisplayName(), "mypolicy");
+    }
+
+    public interface MyInterface {
+    }
+
+    @Catalog(name="Sample No-Op Application",
+            description="Application which does nothing, included only as part of the test cases.",
+            iconUrl="")
+    public static class SampleNoOpApplication extends AbstractApplication implements MyInterface {
+    }
+    
+    public static class MyPolicy extends AbstractPolicy {
+        public MyPolicy() {
+        }
+        public MyPolicy(Map<String, ?> flags) {
+            super(flags);
+        }
+    }
+}