http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/UsageResourceTest.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/UsageResourceTest.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/UsageResourceTest.java
new file mode 100644
index 0000000..21fd246
--- /dev/null
+++ 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/UsageResourceTest.java
@@ -0,0 +1,444 @@
+/*
+ * 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 javax.ws.rs.core.GenericType;
+
+@Test( // by using a different suite name we disallow interleaving other tests 
between the methods of this test class, which wrecks the test fixtures
+        suiteName = "UsageResourceTest")
+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 setUp() {
+        
((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
+        Response response = client().path("/usage/applications").get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        Iterable<UsageStatistics> usages = response.readEntity(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().path("/usage/applications").query("start", 
0).query("end", preStart.getTime().getTime()-1).get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usages = response.readEntity(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().path("/usage/applications").query("start", 
afterPostStart.getTime().getTime()).query("end", 
afterPostStart.getTime().getTime()).get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usages = response.readEntity(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().path("/usage/applications").get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usages = response.readEntity(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().path("/usage/applications").query("start", 
afterPostDelete).get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usages = response.readEntity(new GenericType<List<UsageStatistics>>() 
{});
+        assertTrue(Iterables.isEmpty(usages), "usages="+usages);
+    }
+
+    @Test
+    public void testGetApplicationUsagesForNonExistantApp() throws Exception {
+        Response response = client().path("/usage/applications/wrongid").get();
+        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
+        Response response = client().path("/usage/applications/" + 
appId).get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        UsageStatistics usage = response.readEntity(new 
GenericType<UsageStatistics>() {});
+        assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, 
Status.RUNNING), roundDown(preStart), postStart);
+
+        // Time-constrained requests
+        response = client().path("/usage/applications/" + 
appId).query("start", "1970-01-01T00:00:00-0100").get();
+        usage = response.readEntity(new GenericType<UsageStatistics>() {});
+        assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, 
Status.RUNNING), roundDown(preStart), postStart);
+        
+        response = client().path("/usage/applications/" + 
appId).query("start", "9999-01-01T00:00:00+0100").get();
+        assertTrue(response.getStatus() >= 400, "end defaults to NOW, so 
future start should fail, instead got code "+response.getStatus());
+        
+        response = client().path("/usage/applications/" + 
appId).query("start", "9999-01-01T00:00:00%2B0100").query("end", 
"9999-01-02T00:00:00%2B0100").get();
+        usage = response.readEntity(new GenericType<UsageStatistics>() {});
+        assertTrue(usage.getStatistics().isEmpty());
+
+        response = client().path("/usage/applications/" + appId).query("end", 
"9999-01-01T00:00:00+0100").get();
+        usage = response.readEntity(new GenericType<UsageStatistics>() {});
+        assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, 
Status.RUNNING), roundDown(preStart), postStart);
+
+        response = client().path("/usage/applications/" + 
appId).query("start", "9999-01-01T00:00:00+0100").query("end", 
"9999-02-01T00:00:00+0100").get();
+        usage = response.readEntity(new GenericType<UsageStatistics>() {});
+        assertTrue(usage.getStatistics().isEmpty());
+
+        response = client().path("/usage/applications/" + 
appId).query("start", "1970-01-01T00:00:00-0100").query("end", 
"9999-01-01T00:00:00+0100").get();
+        usage = response.readEntity(new GenericType<UsageStatistics>() {});
+        assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, 
Status.RUNNING), roundDown(preStart), postStart);
+        
+        response = client().path("/usage/applications/" + appId).query("end", 
"1970-01-01T00:00:00-0100").get();
+        usage = response.readEntity(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().path("/usage/applications/" + appId).get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usage = response.readEntity(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().path("/usage/applications/" + 
appId).query("start", afterPostDelete).get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usage = response.readEntity(new GenericType<UsageStatistics>() {});
+        assertTrue(usage.getStatistics().isEmpty(), "usages="+usage);
+    }
+
+    @Test
+    public void testGetMachineUsagesForNonExistantMachine() throws Exception {
+        Response response = client().path("/usage/machines/wrongid").get();
+        assertEquals(response.getStatus(), 
Response.Status.NOT_FOUND.getStatusCode());
+    }
+
+    @Test
+    public void testGetMachineUsagesInitiallyEmpty() throws Exception {
+        // All machines: empty
+        Response response = client().path("/usage/machines").get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        Iterable<UsageStatistics> usages = response.readEntity(new 
GenericType<List<UsageStatistics>>() {});
+        assertTrue(Iterables.isEmpty(usages));
+        
+        // Specific machine that does not exist: get 404
+        response = 
client().path("/usage/machines/machineIdThatDoesNotExist").get();
+        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
+        Response response = client().path("/usage/machines").get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        Iterable<UsageStatistics> usages = response.readEntity(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().path("/usage/machines/"+machine.getId()).get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usage = response.readEntity(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
+        Response response = 
client().path("/usage/machines").query("application", appId).get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        Iterable<UsageStatistics> usages = response.readEntity(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().path("/usage/machines").query("application", 
appId).get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usages = response.readEntity(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().path("/usage/applications").query("start", 
futureTime).get();
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        usages = response.readEntity(new GenericType<List<UsageStatistics>>() 
{});
+        assertTrue(Iterables.isEmpty(usages), "usages="+usages);
+    }
+
+    private String createApp(ApplicationSpec spec) {
+        Response response = clientDeploy(spec);
+        assertEquals(response.getStatus(), 
Response.Status.CREATED.getStatusCode());
+        TaskSummary createTask = response.readEntity(TaskSummary.class);
+        waitForTask(createTask.getId());
+        return createTask.getEntityId();
+    }
+    
+    private void deleteApp(String appId) {
+        Response response = client().path("/applications/"+appId)
+                .delete();
+        assertEquals(response.getStatus(), 
Response.Status.ACCEPTED.getStatusCode());
+        TaskSummary deletionTask = response.readEntity(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() {
+                        Response response = 
client().path("/activities/"+taskId).get();
+                        if (response.getStatus() == 
Response.Status.NOT_FOUND.getStatusCode()) {
+                            return true;
+                        }
+                        TaskSummary summary = 
response.readEntity(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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/VersionResourceTest.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/VersionResourceTest.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/VersionResourceTest.java
new file mode 100644
index 0000000..fad05cc
--- /dev/null
+++ 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/resources/VersionResourceTest.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.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;
+
+@Test(singleThreaded = true,
+        // by using a different suite name we disallow interleaving other 
tests between the methods of this test class, which wrecks the test fixtures
+        suiteName = "VersionResourceTest")
+public class VersionResourceTest extends BrooklynRestResourceTest {
+
+    @Test
+    public void testGetVersion() {
+        Response response = client().path("/version")
+                .get();
+
+        assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
+        String version = response.readEntity(String.class);
+
+        assertTrue(version.matches("^\\d+\\.\\d+\\.\\d+.*"));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/security/PasswordHasherTest.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/security/PasswordHasherTest.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/security/PasswordHasherTest.java
new file mode 100644
index 0000000..575d6a4
--- /dev/null
+++ 
b/rest/rest-resources/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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/test/config/render/TestRendererHints.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/test/config/render/TestRendererHints.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/test/config/render/TestRendererHints.java
new file mode 100644
index 0000000..91294db
--- /dev/null
+++ 
b/rest/rest-resources/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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/test/entity/brooklynnode/DeployBlueprintTest.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/test/entity/brooklynnode/DeployBlueprintTest.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/test/entity/brooklynnode/DeployBlueprintTest.java
new file mode 100644
index 0000000..0113d39
--- /dev/null
+++ 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/test/entity/brooklynnode/DeployBlueprintTest.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.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.test.HttpTestUtils;
+import org.apache.brooklyn.util.guava.Functionals;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.annotations.Test;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest;
+
+public class DeployBlueprintTest extends BrooklynRestResourceTest {
+
+    @Override
+    protected boolean useLocalScannedCatalog() {
+        return true;
+    }
+
+    @Override
+    protected String getEndpointAddress() {
+        return ENDPOINT_ADDRESS_HTTP + "v1";
+    }
+
+    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(ENDPOINT_ADDRESS_HTTP); // BrooklynNode 
will append "/v1" to it
+
+        EntitySpec<BrooklynNode> spec = EntitySpec.create(BrooklynNode.class);
+        EntityManager mgr = getManagementContext().getEntityManager(); // 
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(getEndpointAddress() + 
"/applications");
+        List<String> appType = parseJsonList(apps, ImmutableList.of("spec", 
"type"), String.class);
+        assertEquals(appType, 
ImmutableList.of(BasicApplication.class.getName()));
+
+        String status = 
HttpTestUtils.getContent(getEndpointAddress()+"/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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestApiTest.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestApiTest.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestApiTest.java
new file mode 100644
index 0000000..853afb0
--- /dev/null
+++ 
b/rest/rest-resources/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.util.HashSet;
+import java.util.Set;
+
+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.internal.BrooklynProperties;
+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.server.BrooklynServerConfig;
+import org.apache.brooklyn.core.test.entity.LocalManagementContextForTests;
+import org.apache.brooklyn.rest.BrooklynRestApi;
+import org.apache.brooklyn.rest.util.BrooklynRestResourceUtils;
+import org.apache.brooklyn.rest.util.ManagementContextProvider;
+import org.apache.brooklyn.rest.util.ShutdownHandlerProvider;
+import org.apache.brooklyn.rest.util.TestShutdownHandler;
+import org.apache.brooklyn.rest.util.json.BrooklynJacksonJsonProvider;
+import org.apache.cxf.jaxrs.client.JAXRSClientFactory;
+import org.reflections.util.ClasspathHelper;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public abstract class BrooklynRestApiTest {
+
+    public static final String SCANNING_CATALOG_BOM_URL = 
"classpath://brooklyn/scanning.catalog.bom";
+
+    protected ManagementContext manager;
+    
+    
+    protected TestShutdownHandler shutdownListener = createShutdownHandler();
+    protected final static String ENDPOINT_ADDRESS_LOCAL = "local://";
+    protected final static String ENDPOINT_ADDRESS_HTTP = 
"http://localhost:9998/";;
+
+    protected Set<Class<?>> resourceClasses;
+    protected Set<Object> resourceBeans;
+
+    @BeforeClass(alwaysRun = true)
+    public void setUpClass() throws Exception {
+        if (!isMethodInit()) {
+            initClass();
+        }
+    }
+
+    @AfterClass(alwaysRun = true)
+    public void tearDownClass() throws Exception {
+        if (!isMethodInit()) {
+            destroyClass();
+        }
+    }
+
+    @BeforeMethod(alwaysRun = true)
+    public void setUpMethod() throws Exception {
+        if (isMethodInit()) {
+            initClass();
+        }
+        initMethod();
+    }
+
+    @AfterMethod(alwaysRun = true)
+    public void tearDownMethod() throws Exception {
+        if (isMethodInit()) {
+            destroyClass();
+        }
+        destroyMethod();
+    }
+    
+    protected void initClass() throws Exception {
+        resourceClasses = new HashSet<>();
+        resourceBeans = new HashSet<>();
+    }
+
+    protected void destroyClass() throws Exception {
+        destroyManagementContext();
+        resourceClasses = null;
+        resourceBeans = null;
+    }
+
+    protected void initMethod() throws Exception {
+        resetShutdownListener();
+    }
+
+    protected void destroyMethod() throws Exception {
+    }
+    
+    /**
+     * @return true to start/destroy the test environment for each method.
+     *          Returns false by default to speed up testing.
+     */
+    protected boolean isMethodInit() {
+        return false;
+    }
+
+    protected void resetShutdownListener() {
+        shutdownListener.reset();
+    }
+
+    protected void destroyManagementContext() {
+        if (manager!=null) {
+            Entities.destroyAll(manager);
+            resourceClasses = null;
+            resourceBeans = null;
+            manager = null;
+        }
+    }
+
+    protected boolean useLocalScannedCatalog() {
+        return false;
+    }
+    
+    private TestShutdownHandler createShutdownHandler() {
+        return new TestShutdownHandler();
+    }
+
+    protected synchronized ManagementContext getManagementContext() {
+        if (manager==null) {
+            if (useLocalScannedCatalog()) {
+                manager = new LocalManagementContext();
+                forceUseOfDefaultCatalogWithJavaClassPath();
+            } else {
+                manager = new LocalManagementContextForTests();
+            }
+            manager.getHighAvailabilityManager().disabled();
+            BasicLocationRegistry.setupLocationRegistryForTesting(manager);
+            
+            new BrooklynCampPlatformLauncherNoServer()
+                .useManagementContext(manager)
+                .launch();
+        }
+        return manager;
+    }
+    
+    protected String getEndpointAddress() {
+        return ENDPOINT_ADDRESS_HTTP;
+    }
+
+    protected ObjectMapper mapper() {
+        return BrooklynJacksonJsonProvider.findSharedObjectMapper(null, 
getManagementContext());
+    }
+
+    public LocationRegistry getLocationRegistry() {
+        return new 
BrooklynRestResourceUtils(getManagementContext()).getLocationRegistry();
+    }
+
+    protected final void addResource(Object resource) {
+        if (resource instanceof Class) {
+            resourceClasses.add((Class<?>)resource);
+        } else {
+            resourceBeans.add(resource);
+        }
+        if (resource instanceof ManagementContextInjectable) {
+            
((ManagementContextInjectable)resource).setManagementContext(getManagementContext());
+        }
+    }
+
+    protected final void addProvider(Class<?> provider) {
+        addResource(provider);
+    }
+
+    protected void addDefaultResources() {
+        addResource(new ShutdownHandlerProvider(shutdownListener));
+        addResource(new ManagementContextProvider(getManagementContext()));
+    }
+
+
+    /** 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 final void setUpResources() {
+        addDefaultResources();
+        addBrooklynResources();
+        for (Object r: BrooklynRestApi.getMiscResources())
+            addResource(r);
+    }
+
+    public <T> T resource(Class<T> clazz) {
+        return JAXRSClientFactory.create(getEndpointAddress(), clazz);
+    }
+
+    public <T> T resource(String uri, Class<T> clazz) {
+        return JAXRSClientFactory.create(getEndpointAddress() + uri, clazz);
+    }
+
+    private void forceUseOfDefaultCatalogWithJavaClassPath() {
+        // don't use any catalog.xml which is set
+        
((BrooklynProperties)manager.getConfig()).put(BrooklynServerConfig.BROOKLYN_CATALOG_URL,
 SCANNING_CATALOG_BOM_URL);
+        // sets URLs for a surefire
+        
((LocalManagementContext)manager).setBaseClassPathForScanning(ClasspathHelper.forJavaClassPath());
+        // this also works
+//        
((LocalManagementContext)manager).setBaseClassPathForScanning(ClasspathHelper.forPackage("brooklyn"));
+        // but this (near-default behaviour) does not
+//        
((LocalManagementContext)manager).setBaseClassLoader(getClass().getClassLoader());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestResourceTest.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestResourceTest.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestResourceTest.java
new file mode 100644
index 0000000..505bb58
--- /dev/null
+++ 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/BrooklynRestResourceTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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.io.IOException;
+import java.net.URI;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+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 org.apache.cxf.endpoint.Server;
+import org.apache.cxf.jaxrs.JAXRSServerFactoryBean;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.cxf.jaxrs.utils.ResourceUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testng.Assert;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+
+public abstract class BrooklynRestResourceTest extends BrooklynRestApiTest {
+
+    private static final Logger log = 
LoggerFactory.getLogger(BrooklynRestResourceTest.class);
+
+    private static Server server;
+    protected List<?> clientProviders;
+    
+    class DefaultTestApp extends javax.ws.rs.core.Application {
+        @Override
+        public Set<Class<?>> getClasses() {
+            return resourceClasses;
+        }
+
+        @Override
+        public Set<Object> getSingletons() {
+            return resourceBeans;
+        }
+
+    };
+
+    @Override
+    public void initClass() throws Exception {
+        super.initClass();
+        startServer();
+    }
+
+    @Override
+    public void destroyClass() throws Exception {
+        stopServer();
+        super.destroyClass();
+    }
+
+    protected synchronized void startServer() throws Exception {
+        if (server == null) {
+            setUpResources();
+            JAXRSServerFactoryBean sf = 
ResourceUtils.createApplication(createRestApp(), true);
+            if (clientProviders == null) {
+                clientProviders = sf.getProviders();
+            }
+            configureCXF(sf);
+            sf.setAddress(getEndpointAddress());
+            sf.setFeatures(ImmutableList.of(new 
org.apache.cxf.feature.LoggingFeature()));
+            server = sf.create();
+        }
+    }
+
+    private javax.ws.rs.core.Application createRestApp() {
+        return new DefaultTestApp();
+    }
+
+    /** Allows subclasses to customize the CXF server bean. */
+    protected void configureCXF(JAXRSServerFactoryBean sf) {
+    }
+
+    public synchronized void stopServer() throws Exception {
+        if (server != null) {
+            server.stop();
+            server.destroy();
+            server = null;
+        }
+    }
+
+
+    protected Response 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().path("/applications")
+                .post(Entity.entity(new 
ObjectMapper().writer().writeValueAsBytes(spec), 
MediaType.APPLICATION_OCTET_STREAM));
+        } 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().path(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().path(resource).get(clazz);
+                            return true;
+                        } catch (WebApplicationException 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().path(resource).get(clazz);
+                            return false;
+                        } catch (WebApplicationException e) {
+                            return e.getResponse().getStatus() == 404;
+                        }
+                    }
+                })
+                .every(1, TimeUnit.SECONDS)
+                .limitTimeTo(30, TimeUnit.SECONDS)
+                .run();
+        assertTrue(success);
+    }
+
+    protected static Entity<byte[]> toJsonEntity(Object obj) throws 
IOException {
+        // TODO: figure out how to have CXF actually send empty maps instead 
of replacing them with nulls without this workaround
+        // see cxf's AbstractClient.checkIfBodyEmpty
+        return Entity.entity(new 
ObjectMapper().writer().writeValueAsBytes(obj), MediaType.APPLICATION_JSON);
+    }
+
+    public WebClient client() {
+        return WebClient.create(getEndpointAddress(), clientProviders);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/CapitalizePolicy.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/CapitalizePolicy.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/CapitalizePolicy.java
new file mode 100644
index 0000000..7d80a6f
--- /dev/null
+++ 
b/rest/rest-resources/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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroup.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroup.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroup.java
new file mode 100644
index 0000000..707c4a3
--- /dev/null
+++ 
b/rest/rest-resources/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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroupImpl.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroupImpl.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/EverythingGroupImpl.java
new file mode 100644
index 0000000..8b2c98b
--- /dev/null
+++ 
b/rest/rest-resources/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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroup.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroup.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroup.java
new file mode 100644
index 0000000..f9a2e21
--- /dev/null
+++ 
b/rest/rest-resources/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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroupImpl.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroupImpl.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/NameMatcherGroupImpl.java
new file mode 100644
index 0000000..bec416f
--- /dev/null
+++ 
b/rest/rest-resources/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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockApp.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockApp.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockApp.java
new file mode 100644
index 0000000..6d92e65
--- /dev/null
+++ 
b/rest/rest-resources/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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockAppBuilder.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockAppBuilder.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockAppBuilder.java
new file mode 100644
index 0000000..1ca10bd
--- /dev/null
+++ 
b/rest/rest-resources/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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimpleEntity.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimpleEntity.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimpleEntity.java
new file mode 100644
index 0000000..58d24aa
--- /dev/null
+++ 
b/rest/rest-resources/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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimplePolicy.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimplePolicy.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/testing/mocks/RestMockSimplePolicy.java
new file mode 100644
index 0000000..e15cdd1
--- /dev/null
+++ 
b/rest/rest-resources/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/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/BrooklynRestResourceUtilsTest.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/BrooklynRestResourceUtilsTest.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/BrooklynRestResourceUtilsTest.java
new file mode 100644
index 0000000..48908e3
--- /dev/null
+++ 
b/rest/rest-resources/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);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/EntityLocationUtilsTest.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/EntityLocationUtilsTest.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/EntityLocationUtilsTest.java
new file mode 100644
index 0000000..f0c65e4
--- /dev/null
+++ 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/EntityLocationUtilsTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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 java.util.Arrays;
+import java.util.Map;
+
+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.core.entity.Entities;
+import org.apache.brooklyn.core.location.AbstractLocation;
+import org.apache.brooklyn.core.location.geo.HostGeoInfo;
+import org.apache.brooklyn.core.location.internal.LocationInternal;
+import org.apache.brooklyn.core.test.BrooklynAppUnitTestSupport;
+import org.apache.brooklyn.entity.software.base.SoftwareProcess;
+import org.apache.brooklyn.rest.testing.mocks.RestMockSimpleEntity;
+
+import com.google.common.collect.ImmutableList;
+
+public class EntityLocationUtilsTest extends BrooklynAppUnitTestSupport {
+
+    private static final Logger log = 
LoggerFactory.getLogger(EntityLocationUtilsTest.class);
+    
+    private Location loc;
+    
+    @BeforeMethod(alwaysRun=true)
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+        loc = mgmt.getLocationRegistry().resolve("localhost");
+        ((AbstractLocation)loc).setHostGeoInfo(new HostGeoInfo("localhost", 
"localhost", 50, 0));
+    }
+    
+    @Test
+    public void testCount() {
+        @SuppressWarnings("unused")
+        SoftwareProcess r1 = 
app.createAndManageChild(EntitySpec.create(SoftwareProcess.class, 
RestMockSimpleEntity.class));
+        SoftwareProcess r2 = 
app.createAndManageChild(EntitySpec.create(SoftwareProcess.class, 
RestMockSimpleEntity.class));
+        Entities.start(app, Arrays.<Location>asList(loc));
+
+        Entities.dumpInfo(app);
+
+        log.info("r2loc: "+r2.getLocations());
+        log.info("props: 
"+((LocationInternal)r2.getLocations().iterator().next()).config().getBag().getAllConfig());
+
+        Map<Location, Integer> counts = new 
EntityLocationUtils(mgmt).countLeafEntitiesByLocatedLocations();
+        log.info("count: "+counts);
+        assertEquals(ImmutableList.copyOf(counts.values()), 
ImmutableList.of(2), "counts="+counts);
+    }
+}

http://git-wip-us.apache.org/repos/asf/brooklyn-server/blob/6f624c78/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/HaHotStateCheckClassResource.java
----------------------------------------------------------------------
diff --git 
a/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/HaHotStateCheckClassResource.java
 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/HaHotStateCheckClassResource.java
new file mode 100644
index 0000000..80e9c46
--- /dev/null
+++ 
b/rest/rest-resources/src/test/java/org/apache/brooklyn/rest/util/HaHotStateCheckClassResource.java
@@ -0,0 +1,38 @@
+/*
+ * 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 javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.apache.brooklyn.rest.filter.HaHotStateRequired;
+
+@Path("/ha/class")
+@Produces(MediaType.APPLICATION_JSON)
+@HaHotStateRequired
+public class HaHotStateCheckClassResource {
+
+    @GET
+    @Path("fail")
+    public String fail() {
+        return "FAIL";
+    }
+}

Reply via email to