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;
+ }
+}