Test harness for REST API unit tests

- Unit test for the GET functions of the
  High Level Intents API (/wm/onos/intent/high)

- Test REST API webserver that can be used by
  JUnit tests to launch a web server that will
  respond to REST requests

- Intent mocking class to be shared by all tests
  that use Intents.

Change-Id: Ib69ebb3d8b4f574044acc8d6ba39db66a2e08a47
diff --git a/src/test/java/net/onrc/onos/api/rest/TestRestApiServer.java b/src/test/java/net/onrc/onos/api/rest/TestRestApiServer.java
new file mode 100644
index 0000000..e5c0634
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/rest/TestRestApiServer.java
@@ -0,0 +1,188 @@
+package net.onrc.onos.api.rest;
+
+
+import net.floodlightcontroller.restserver.RestletRoutable;
+import net.onrc.onos.core.intent.runtime.web.IntentWebRoutable;
+import org.restlet.Application;
+import org.restlet.Component;
+import org.restlet.Context;
+import org.restlet.Request;
+import org.restlet.Response;
+import org.restlet.Restlet;
+import org.restlet.Server;
+import org.restlet.data.Protocol;
+import org.restlet.data.Reference;
+import org.restlet.data.Status;
+import org.restlet.ext.jackson.JacksonRepresentation;
+import org.restlet.representation.Representation;
+import org.restlet.routing.Filter;
+import org.restlet.routing.Router;
+import org.restlet.routing.Template;
+import org.restlet.service.StatusService;
+
+import java.util.List;
+
+/**
+ * A REST API server suitible for inclusion in unit tests.  Unit tests can
+ * create a server on a given port, then specify the RestletRoutable classes
+ * that are to be tested.  The lifecyle for the server is to create it
+ * and then start it during the @Before (setUp) portion of the test and to
+ * shut it down during the @After (tearDown) section.
+ */
+public class TestRestApiServer {
+
+    private List<RestletRoutable> restlets;
+    private RestApplication restApplication;
+    private Server server;
+    private Component component;
+    private int port;
+
+    /**
+     * Hide the default constructor.
+     */
+    @SuppressWarnings("unused")
+    private TestRestApiServer() { }
+
+    /**
+     * Public constructor.  Given a port number, create a REST API server on
+     * that port.  The server is not running, it can be started using the
+     * startServer() method.
+     * @param serverPort port for the server to listen on.
+     */
+    public TestRestApiServer(final int serverPort) {
+        port = serverPort;
+    }
+
+    /**
+     * The restlet engine requires an Application as a container.
+     */
+    private class RestApplication extends Application {
+        private Context context;
+
+        /**
+         * Initialize the Application along with its Context.
+         */
+        public RestApplication() {
+            super();
+            context = new Context();
+        }
+
+        /**
+         * Add an attribute to the Context for the Application.  This is most
+         * often used to specify attributes that allow modules to locate each
+         * other.
+         *
+         * @param name name of the attribute
+         * @param value value of the attribute
+         */
+        public void addAttribute(final String name, final Object value) {
+            context.getAttributes().put(name, value);
+        }
+
+        /**
+         * Sets up the Restlet for the APIs under test using a Router.  Also, a
+         * filter is installed to deal with double slashes in URLs.
+         * This code is adapted from
+         * net.floodlightcontroller.restserver.RestApiServer
+         *
+         * @return Router object for the APIs under test.
+         */
+        @Override
+        public Restlet createInboundRoot() {
+            Router baseRouter = new Router(context);
+            baseRouter.setDefaultMatchingMode(Template.MODE_STARTS_WITH);
+            for (RestletRoutable rr : restlets) {
+                baseRouter.attach(rr.basePath(), rr.getRestlet(context));
+            }
+
+            /**
+             * Filter out multiple slashes in URLs to make them a single slash.
+             */
+            Filter slashFilter = new Filter() {
+                @Override
+                protected int beforeHandle(Request request, Response response) {
+                    Reference ref = request.getResourceRef();
+                    String originalPath = ref.getPath();
+                    if (originalPath.contains("//")) {
+                        String newPath = originalPath.replaceAll("/+", "/");
+                        ref.setPath(newPath);
+                    }
+                    return Filter.CONTINUE;
+                }
+
+            };
+            slashFilter.setNext(baseRouter);
+
+            return slashFilter;
+        }
+
+        /**
+         * Run the Application on a given port.
+         *
+         * @param restPort port to listen on for inbounde requests
+         */
+        public void run(final int restPort) {
+            setStatusService(new StatusService() {
+                @Override
+                public Representation getRepresentation(Status status,
+                                                        Request request,
+                                                        Response response) {
+                    return new JacksonRepresentation<>(status);
+                }
+            });
+
+            // Start listening for REST requests
+            try {
+                component = new Component();
+                server = component.getServers().add(Protocol.HTTP, restPort);
+                component.getDefaultHost().attach(this);
+                component.start();
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    /**
+     * Start up the REST server.  A list of the Restlets being tested is
+     * passed in.  The usual use of this method is in the @Before (startUp)
+     * of a JUnit test.
+     *
+     * @param restletsUnderTest list of Restlets to run as part of the server.
+     * @throws Exception if starting the server fails.
+     */
+    public void startServer(final List<RestletRoutable> restletsUnderTest)
+           throws Exception {
+        restlets = restletsUnderTest;
+
+        restApplication = new RestApplication();
+        restApplication.run(port);
+
+    }
+
+    /**
+     * Stop the REST server.  The container is stopped, and the server will
+     * no longer respond to requests.  The usual use of this is in the @After
+     * (tearDown) part of the server.
+     *
+     * @throws Exception if the server cannot be shut down cleanly.
+     */
+    public void stopServer() throws Exception {
+        restApplication.stop();
+        server.stop();
+        component.stop();
+    }
+
+
+    /**
+     * Add an attribute to the Context for the Application.  This is most
+     * often used to specify attributes that allow modules to locate each
+     * other.
+     *
+     * @param name name of the attribute
+     * @param value value of the attribute
+     */
+    public void addAttribute(final String name, final Object value) {
+        restApplication.addAttribute(name, value);
+    }
+}
diff --git a/src/test/java/net/onrc/onos/api/rest/TestRestIntentHighGet.java b/src/test/java/net/onrc/onos/api/rest/TestRestIntentHighGet.java
new file mode 100644
index 0000000..90bf596
--- /dev/null
+++ b/src/test/java/net/onrc/onos/api/rest/TestRestIntentHighGet.java
@@ -0,0 +1,264 @@
+package net.onrc.onos.api.rest;
+
+
+import net.floodlightcontroller.core.module.FloodlightModuleContext;
+import net.floodlightcontroller.restserver.RestletRoutable;
+import net.onrc.onos.core.intent.IntentOperation;
+import net.onrc.onos.core.intent.IntentOperationList;
+import net.onrc.onos.core.intent.ShortestPathIntent;
+import net.onrc.onos.core.intent.runtime.IPathCalcRuntimeService;
+import net.onrc.onos.core.intent.runtime.IntentTestMocks;
+import net.onrc.onos.core.intent.runtime.PathCalcRuntimeModule;
+import net.onrc.onos.core.intent.runtime.web.IntentWebRoutable;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.restlet.data.Status;
+import org.restlet.resource.ClientResource;
+import org.restlet.resource.ResourceException;
+
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasKey;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+/**
+ * Unit tests to test the Intents REST APIs.
+ */
+@RunWith(PowerMockRunner.class)
+@PrepareForTest(PathCalcRuntimeModule.class)
+public class TestRestIntentHighGet {
+    private static final Long LOCAL_PORT = 0xFFFEL;
+
+    private static int REST_PORT = 7777;
+    private static String HOST_BASE_URL = "http://localhost:" +
+                                          Integer.toString(REST_PORT);
+    private static final String BASE_URL = HOST_BASE_URL + "/wm/onos/intent";
+    private static final String HIGH_URL = BASE_URL + "/high";
+
+    private PathCalcRuntimeModule runtime;
+    private TestRestApiServer restApiServer;
+    private IntentTestMocks mocks;
+
+
+    /**
+     * Create the web server, PathCalcRuntime, and mocks required for
+     * all of the tests.
+     * @throws Exception if the mocks or webserver cannot be started.
+     */
+    @Before
+    public void setUp() throws Exception {
+        mocks = new IntentTestMocks();
+        mocks.setUpIntentMocks();
+
+        runtime = new PathCalcRuntimeModule();
+        final FloodlightModuleContext moduleContext = mocks.getModuleContext();
+        runtime.init(moduleContext);
+        runtime.startUp(moduleContext);
+
+        final List<RestletRoutable> restlets = new LinkedList<>();
+        restlets.add(new IntentWebRoutable());
+
+        restApiServer = new TestRestApiServer(REST_PORT);
+        restApiServer.startServer(restlets);
+        restApiServer.addAttribute(IPathCalcRuntimeService.class.getCanonicalName(),
+                                   runtime);
+    }
+
+
+    /**
+     * Remove anything that will interfere with the next test running correctly.
+     * Shuts down the test REST web server and removes the mocks.
+     * @throws Exception if the mocks can't be removed or the web server can't
+     *         shut down correctly.
+     */
+    @After
+    public void tearDown() throws Exception {
+        restApiServer.stopServer();
+        mocks.tearDownIntentMocks();
+    }
+
+
+    /**
+     * Make a set of Intents that can be used as test data.
+     */
+    private void makeDefaultIntents() {
+        final String BAD_SWITCH_INTENT_NAME = "No Such Switch Intent";
+
+        // create shortest path intents
+        final IntentOperationList opList = new IntentOperationList();
+        opList.add(IntentOperation.Operator.ADD,
+                new ShortestPathIntent(BAD_SWITCH_INTENT_NAME, 111L, 12L,
+                        LOCAL_PORT, 2L, 21L, LOCAL_PORT));
+        opList.add(IntentOperation.Operator.ADD,
+                new ShortestPathIntent("1:2", 1L, 14L, LOCAL_PORT, 4L, 41L,
+                        LOCAL_PORT));
+        opList.add(IntentOperation.Operator.ADD,
+                new ShortestPathIntent("1:3", 2L, 23L, LOCAL_PORT, 3L, 32L,
+                        LOCAL_PORT));
+
+        // compile high-level intent operations into low-level intent
+        // operations (calculate paths)
+
+        final IntentOperationList pathIntentOpList =
+                runtime.executeIntentOperations(opList);
+        assertThat(pathIntentOpList, notNullValue());
+
+    }
+
+    /**
+     * Utility function to locate an intent in a JSON collection
+     * that has the given id.
+     * The JSON collection of intents looks like:
+     *  <code>
+     *      MAP =
+     *        [0] =
+     *          MAP =
+     *            id = "1"
+     *            ...
+     *        [1]
+     *          MAP =
+     *            id = "2"
+     *            ...
+     *        [2]
+     *          MAP =
+     *            id = "3"
+     *            ...
+     *        ...
+     *  </code>
+     *
+     * @param intents collection map to search
+     * @param id id of the intent to look for
+     * @return map for the intent if one was found, null otherwise
+     */
+    private Map<String, String> findIntentWithId(final Collection<Map<String, String>> intents,
+                                                 final String id) {
+        for (final Map<String, String>intentMap : intents) {
+            if (id.equals(intentMap.get("id"))) {
+                return intentMap;
+            }
+        }
+        return null;
+    }
+
+
+    /**
+     * Convenience function to fetch a collection of Intents from the JSON
+     * result of a REST call.  Hides the ugliness of the unchecked conversion
+     * to the proper Collection of Map type.
+     *
+     * @param client ClientResource that was used to make the REST call
+     * @return Collection of Maps that hold the Intent data
+     */
+    @SuppressWarnings("unchecked")
+    private Collection<Map<String, String>> getIntentsCollection (final ClientResource client) {
+        return (Collection<Map<String, String>>)client.get(Collection.class);
+    }
+
+    /**
+     * Convenience function to fetch a single Intent from the JSON
+     * result of a REST call.  Hides the ugliness of the unchecked conversion
+     * to the proper Map type.
+     *
+     * @param client ClientResource that was used to make the REST call
+     * @return Map that hold the Intent data
+     */
+    @SuppressWarnings("unchecked")
+    private Map<String, String> getIntent (final ClientResource client) {
+        return (Map<String, String>)client.get(Map.class);
+    }
+
+    /**
+     * Test that the GET of all Intents REST call returns the proper result.
+     * The call to get all Intents should return 3 items, an HTTP status of OK,
+     * and the proper Intent data.
+     *
+     * @throws Exception if any of the set up or tear down operations fail
+     */
+    @Test
+    public void testFetchOfAllIntents() throws Exception {
+
+        makeDefaultIntents();
+
+        final ClientResource client = new ClientResource(HIGH_URL);
+        final Collection<Map<String, String>> intents = getIntentsCollection(client);
+
+        // HTTP status should be OK
+        assertThat(client.getStatus(), is(equalTo(Status.SUCCESS_OK)));
+
+        // 3 intents should have been fetched
+        assertThat(intents, hasSize(3));
+
+        // check that the Intent with id "3" is present, and has the right data
+        final Map<String, String> mapForIntent3 = findIntentWithId(intents, "1:3");
+        // Intent 3 must exist
+        assertThat(mapForIntent3, notNullValue());
+        //  Data must be correct
+        assertThat(mapForIntent3, hasKey("state"));
+        final String state = mapForIntent3.get("state");
+        assertThat(state, is(equalTo("INST_REQ")));
+    }
+
+    /**
+     * Test that the GET of a single Intent REST call returns the proper result
+     * when given a bad Intent id. The call to get the Intent should return a
+     * status of NOT_FOUND.
+     *
+     * @throws Exception if any of the set up or tear down operations fail
+     */
+    @Test
+    public void testFetchOfBadIntent() throws Exception {
+
+        makeDefaultIntents();
+
+        final ClientResource client = new ClientResource(HIGH_URL + "/2334");
+
+        try {
+            getIntent(client);
+            // The get operation should have thrown a ResourceException.
+            // Fail because the Exception was not seen.
+            Assert.fail("Invalid intent fetch did not cause an exception");
+        } catch (ResourceException resourceError) {
+            // The HTTP status should be NOT FOUND
+            assertThat(client.getStatus(), is(equalTo(Status.CLIENT_ERROR_NOT_FOUND)));
+        }
+    }
+
+    /**
+     * Test that the GET of a single Intent REST call returns the proper result
+     * for an existing Intent. The call to get the Intent should return a
+     * status of OK, and the data for the Intent should be correct.
+     *
+     * @throws Exception if any of the set up or tear down operations fail
+     */
+    @Test
+    public void testFetchOfGoodIntent() throws Exception {
+
+        makeDefaultIntents();
+
+        final ClientResource client = new ClientResource(HIGH_URL + "/2");
+        final Map<String, String> intent;
+        intent = getIntent(client);
+
+        // HTTP status should be OK
+        assertThat(client.getStatus(), is(equalTo(Status.SUCCESS_OK)));
+
+        //  Intent data should be correct
+        assertThat(intent, is(notNullValue()));
+        assertThat(intent, hasKey("id"));
+        assertThat(intent.get("id"), is(equalTo("1:2")));
+        assertThat(intent, hasKey("state"));
+        assertThat(intent.get("state"), is(equalTo("INST_REQ")));
+    }
+}
diff --git a/src/test/java/net/onrc/onos/core/intent/runtime/IntentTestMocks.java b/src/test/java/net/onrc/onos/core/intent/runtime/IntentTestMocks.java
new file mode 100644
index 0000000..97ff808
--- /dev/null
+++ b/src/test/java/net/onrc/onos/core/intent/runtime/IntentTestMocks.java
@@ -0,0 +1,139 @@
+package net.onrc.onos.core.intent.runtime;
+
+
+import net.floodlightcontroller.core.module.FloodlightModuleContext;
+import net.floodlightcontroller.restserver.IRestApiService;
+import net.onrc.onos.core.datagrid.IDatagridService;
+import net.onrc.onos.core.datagrid.IEventChannel;
+import net.onrc.onos.core.datagrid.IEventChannelListener;
+import net.onrc.onos.core.intent.IntentOperationList;
+import net.onrc.onos.core.intent.MockTopology;
+import net.onrc.onos.core.intent.runtime.web.IntentWebRoutable;
+import net.onrc.onos.core.registry.IControllerRegistryService;
+import net.onrc.onos.core.topology.ITopologyListener;
+import net.onrc.onos.core.topology.ITopologyService;
+import org.powermock.api.easymock.PowerMock;
+
+import static org.easymock.EasyMock.anyObject;
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.eq;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.expectLastCall;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+/**
+ * This class contains all of the mocked code required to run a test that uses
+ * the Intent framework.  The normal lifecycle of an object of this class is to
+ * create the object and call setUpIntentMocks() in the @Before (setUp()) part of
+ * the test, then call tearDownIntentMocks() in the @After (tearDown()) part
+ * of the test.  The intention of this class is to hide the mocking mechanics
+ * and the unchecked suppressions in one place, and to make the Intent testing
+ * reusable.
+ *
+ * This code is largely refactored from
+ * net.onrc.onos.core.intent.runtime.UseCaseTest
+ */
+public class IntentTestMocks {
+
+    private FloodlightModuleContext moduleContext;
+    private IDatagridService datagridService;
+    private ITopologyService topologyService;
+    private IControllerRegistryService controllerRegistryService;
+    private PersistIntent persistIntent;
+    private IRestApiService restApi;
+
+    /**
+     * Default constructor.  Doesn't do anything interesting.
+     */
+    public IntentTestMocks() { }
+
+    /**
+     * Create whatever mocks are required to access the Intents framework.
+     * This method is intended to be called during @Before processing (setUp())
+     * of the JUnit test.
+     *
+     * @throws Exception if any of the mocks cannot be created.
+     */
+    @SuppressWarnings("unchecked")
+    public void setUpIntentMocks() throws Exception {
+        final MockTopology topology = new MockTopology();
+        topology.createSampleTopology1();
+
+        datagridService = createMock(IDatagridService.class);
+        topologyService = createMock(ITopologyService.class);
+        controllerRegistryService = createMock(IControllerRegistryService.class);
+        moduleContext = createMock(FloodlightModuleContext.class);
+        final IEventChannel<Long, IntentOperationList> intentOperationChannel =
+                createMock(IEventChannel.class);
+        final IEventChannel<Long, IntentStateList>intentStateChannel =
+                createMock(IEventChannel.class);
+        persistIntent = PowerMock.createMock(PersistIntent.class);
+        restApi = createMock(IRestApiService.class);
+
+        PowerMock.expectNew(PersistIntent.class,
+                anyObject(IControllerRegistryService.class)).andReturn(persistIntent);
+
+        expect(moduleContext.getServiceImpl(IDatagridService.class))
+                .andReturn(datagridService).once();
+        expect(moduleContext.getServiceImpl(ITopologyService.class))
+                .andReturn(topologyService).once();
+        expect(moduleContext.getServiceImpl(IControllerRegistryService.class))
+                .andReturn(controllerRegistryService).once();
+        expect(persistIntent.getKey()).andReturn(1L).anyTimes();
+        expect(persistIntent.persistIfLeader(eq(1L),
+                anyObject(IntentOperationList.class))).andReturn(true)
+                .anyTimes();
+        expect(moduleContext.getServiceImpl(IRestApiService.class))
+                .andReturn(restApi).once();
+
+        expect(topologyService.getTopology()).andReturn(topology)
+                .anyTimes();
+        topologyService.registerTopologyListener(
+                anyObject(ITopologyListener.class));
+        expectLastCall();
+
+        expect(datagridService.createChannel("onos.pathintent",
+                Long.class, IntentOperationList.class))
+                .andReturn(intentOperationChannel).once();
+
+        expect(datagridService.addListener(
+                eq("onos.pathintent_state"),
+                anyObject(IEventChannelListener.class),
+                eq(Long.class),
+                eq(IntentStateList.class)))
+                .andReturn(intentStateChannel).once();
+        restApi.addRestletRoutable(anyObject(IntentWebRoutable.class));
+
+        replay(datagridService);
+        replay(topologyService);
+        replay(moduleContext);
+        replay(controllerRegistryService);
+        PowerMock.replay(persistIntent, PersistIntent.class);
+        replay(restApi);
+    }
+
+    /**
+     * Remove whatever mocks were put in place.  This method is intended to be
+     * called as part of @After processing (tearDown()) of the JUnit test.
+     */
+    public void tearDownIntentMocks() {
+        verify(datagridService);
+        verify(topologyService);
+        verify(moduleContext);
+        verify(controllerRegistryService);
+        PowerMock.verify(persistIntent, PersistIntent.class);
+        verify(restApi);
+    }
+
+    /**
+     * Fetch the Floodligh module context being used by the mock.  Some tests
+     * will need to add items to the Context to allow communications with
+     * downstream classes.
+     *
+     * @return the FloodlightModuleCOntext used by the mock.
+     */
+    public FloodlightModuleContext getModuleContext() {
+        return moduleContext;
+    }
+}