/* * Copyright 2016-present Open Networking Laboratory * * Licensed 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.onosproject.rest.resources; import java.io.InputStream; import java.net.HttpURLConnection; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.ws.rs.NotFoundException; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.hamcrest.Description; import org.hamcrest.Matchers; import org.hamcrest.TypeSafeMatcher; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.onlab.osgi.ServiceDirectory; import org.onlab.osgi.TestServiceDirectory; import org.onlab.packet.MacAddress; import org.onlab.rest.BaseResource; import org.onosproject.app.ApplicationService; import org.onosproject.codec.CodecService; import org.onosproject.codec.impl.CodecManager; import org.onosproject.codec.impl.FlowRuleCodec; import org.onosproject.core.CoreService; import org.onosproject.core.GroupId; import org.onosproject.net.DefaultDevice; import org.onosproject.net.Device; import org.onosproject.net.DeviceId; import org.onosproject.net.NetTestTools; import org.onosproject.net.device.DeviceService; import org.onosproject.net.flow.DefaultTrafficSelector; import org.onosproject.net.flow.DefaultTrafficTreatment; import org.onosproject.net.flow.FlowEntry; import org.onosproject.net.flow.FlowEntryAdapter; import org.onosproject.net.flow.FlowId; import org.onosproject.net.flow.FlowRule; import org.onosproject.net.flow.FlowRuleService; import org.onosproject.net.flow.TrafficSelector; import org.onosproject.net.flow.TrafficTreatment; import org.onosproject.net.flow.criteria.Criterion; import org.onosproject.net.flow.instructions.Instruction; import com.eclipsesource.json.Json; import com.eclipsesource.json.JsonArray; import com.eclipsesource.json.JsonObject; import com.google.common.collect.ImmutableSet; import static java.util.concurrent.TimeUnit.SECONDS; import static org.easymock.EasyMock.anyObject; import static org.easymock.EasyMock.anyShort; import static org.easymock.EasyMock.anyString; import static org.easymock.EasyMock.createMock; import static org.easymock.EasyMock.expect; import static org.easymock.EasyMock.expectLastCall; import static org.easymock.EasyMock.replay; import static org.easymock.EasyMock.verify; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.onosproject.net.NetTestTools.APP_ID; /** * Unit tests for Flows REST APIs. */ public class FlowsResourceTest extends ResourceTest { final FlowRuleService mockFlowService = createMock(FlowRuleService.class); CoreService mockCoreService = createMock(CoreService.class); final HashMap<DeviceId, Set<FlowEntry>> rules = new HashMap<>(); final DeviceService mockDeviceService = createMock(DeviceService.class); final DeviceId deviceId1 = DeviceId.deviceId("1"); final DeviceId deviceId2 = DeviceId.deviceId("2"); final DeviceId deviceId3 = DeviceId.deviceId("3"); final Device device1 = new DefaultDevice(null, deviceId1, Device.Type.OTHER, "", "", "", "", null); final Device device2 = new DefaultDevice(null, deviceId2, Device.Type.OTHER, "", "", "", "", null); final ApplicationService mockApplicationService = createMock(ApplicationService.class); final MockFlowEntry flow1 = new MockFlowEntry(deviceId1, 1); final MockFlowEntry flow2 = new MockFlowEntry(deviceId1, 2); final MockFlowEntry flow3 = new MockFlowEntry(deviceId2, 3); final MockFlowEntry flow4 = new MockFlowEntry(deviceId2, 4); final MockFlowEntry flow5 = new MockFlowEntry(deviceId2, 5); final MockFlowEntry flow6 = new MockFlowEntry(deviceId2, 6); final Set<FlowEntry> flowEntries = ImmutableSet.of(flow1, flow2, flow3, flow4, flow5, flow6); /** * Mock class for a flow entry. */ private static class MockFlowEntry extends FlowEntryAdapter { final DeviceId deviceId; final long baseValue; TrafficTreatment treatment; TrafficSelector selector; public MockFlowEntry(DeviceId deviceId, long id) { this.deviceId = deviceId; this.baseValue = id * 100; } @Override public FlowEntryState state() { return FlowEntryState.ADDED; } @Override public long life() { return life(SECONDS); } @Override public long life(TimeUnit timeUnit) { return SECONDS.convert(baseValue + 11, timeUnit); } @Override public FlowLiveType liveType() { return FlowLiveType.IMMEDIATE; } @Override public long packets() { return baseValue + 22; } @Override public long bytes() { return baseValue + 33; } @Override public long lastSeen() { return baseValue + 44; } @Override public FlowId id() { final long id = baseValue + 55; return FlowId.valueOf(id); } @Override public GroupId groupId() { return new GroupId(3); } @Override public short appId() { return 2; } @Override public int priority() { return (int) (baseValue + 66); } @Override public DeviceId deviceId() { return deviceId; } @Override public TrafficSelector selector() { return selector; } @Override public TrafficTreatment treatment() { return treatment; } @Override public int timeout() { return (int) (baseValue + 77); } @Override public FlowRemoveReason reason() { return FlowRemoveReason.NO_REASON; } } /** * Populates some flows used as testing data. */ private void setupMockFlows() { flow2.treatment = DefaultTrafficTreatment.builder() .setEthDst(MacAddress.BROADCAST) .build(); flow2.selector = DefaultTrafficSelector.builder() .matchEthType((short) 3) .matchIPProtocol((byte) 9) .build(); flow4.treatment = DefaultTrafficTreatment.builder() .build(); final Set<FlowEntry> flows1 = new HashSet<>(); flows1.add(flow1); flows1.add(flow2); final Set<FlowEntry> flows2 = new HashSet<>(); flows2.add(flow3); flows2.add(flow4); rules.put(deviceId1, flows1); rules.put(deviceId2, flows2); expect(mockFlowService.getFlowEntries(deviceId1)) .andReturn(rules.get(deviceId1)).anyTimes(); expect(mockFlowService.getFlowEntries(deviceId2)) .andReturn(rules.get(deviceId2)).anyTimes(); } /** * Sets up the global values for all the tests. */ @Before public void setUpTest() { // Mock device service expect(mockDeviceService.getDevice(deviceId1)) .andReturn(device1); expect(mockDeviceService.getDevice(deviceId2)) .andReturn(device2); expect(mockDeviceService.getDevices()) .andReturn(ImmutableSet.of(device1, device2)); // Mock Core Service expect(mockCoreService.getAppId(anyShort())) .andReturn(NetTestTools.APP_ID).anyTimes(); expect(mockCoreService.getAppId(anyString())) .andReturn(NetTestTools.APP_ID).anyTimes(); expect(mockCoreService.registerApplication(FlowRuleCodec.REST_APP_ID)) .andReturn(APP_ID).anyTimes(); replay(mockCoreService); // Register the services needed for the test final CodecManager codecService = new CodecManager(); codecService.activate(); ServiceDirectory testDirectory = new TestServiceDirectory() .add(FlowRuleService.class, mockFlowService) .add(DeviceService.class, mockDeviceService) .add(CodecService.class, codecService) .add(CoreService.class, mockCoreService) .add(ApplicationService.class, mockApplicationService); BaseResource.setServiceDirectory(testDirectory); } /** * Cleans up and verifies the mocks. */ @After public void tearDownTest() { verify(mockFlowService); verify(mockCoreService); } /** * Hamcrest matcher to check that a flow representation in JSON matches * the actual flow entry. */ public static class FlowEntryJsonMatcher extends TypeSafeMatcher<JsonObject> { private final FlowEntry flow; private final String expectedAppId; private String reason = ""; public FlowEntryJsonMatcher(FlowEntry flowValue, String expectedAppIdValue) { flow = flowValue; expectedAppId = expectedAppIdValue; } @Override public boolean matchesSafely(JsonObject jsonFlow) { // check id final String jsonId = jsonFlow.get("id").asString(); final String flowId = Long.toString(flow.id().value()); if (!jsonId.equals(flowId)) { reason = "id " + flow.id().toString(); return false; } // check application id final String jsonAppId = jsonFlow.get("appId").asString(); if (!jsonAppId.equals(expectedAppId)) { reason = "appId " + Short.toString(flow.appId()); return false; } // check device id final String jsonDeviceId = jsonFlow.get("deviceId").asString(); if (!jsonDeviceId.equals(flow.deviceId().toString())) { reason = "deviceId " + flow.deviceId(); return false; } // check treatment and instructions array if (flow.treatment() != null) { final JsonObject jsonTreatment = jsonFlow.get("treatment").asObject(); final JsonArray jsonInstructions = jsonTreatment.get("instructions").asArray(); if (flow.treatment().immediate().size() != jsonInstructions.size()) { reason = "instructions array size of " + Integer.toString(flow.treatment().immediate().size()); return false; } for (final Instruction instruction : flow.treatment().immediate()) { boolean instructionFound = false; for (int instructionIndex = 0; instructionIndex < jsonInstructions.size(); instructionIndex++) { final String jsonType = jsonInstructions.get(instructionIndex) .asObject().get("type").asString(); final String instructionType = instruction.type().name(); if (jsonType.equals(instructionType)) { instructionFound = true; } } if (!instructionFound) { reason = "instruction " + instruction.toString(); return false; } } } // check selector and criteria array if (flow.selector() != null) { final JsonObject jsonTreatment = jsonFlow.get("selector").asObject(); final JsonArray jsonCriteria = jsonTreatment.get("criteria").asArray(); if (flow.selector().criteria().size() != jsonCriteria.size()) { reason = "criteria array size of " + Integer.toString(flow.selector().criteria().size()); return false; } for (final Criterion criterion : flow.selector().criteria()) { boolean criterionFound = false; for (int criterionIndex = 0; criterionIndex < jsonCriteria.size(); criterionIndex++) { final String jsonType = jsonCriteria.get(criterionIndex) .asObject().get("type").asString(); final String criterionType = criterion.type().name(); if (jsonType.equals(criterionType)) { criterionFound = true; } } if (!criterionFound) { reason = "criterion " + criterion.toString(); return false; } } } return true; } @Override public void describeTo(Description description) { description.appendText(reason); } } /** * Factory to allocate a flow matcher. * * @param flow flow object we are looking for * @return matcher */ private static FlowEntryJsonMatcher matchesFlow(FlowEntry flow, String expectedAppName) { return new FlowEntryJsonMatcher(flow, expectedAppName); } /** * Hamcrest matcher to check that a flow is represented properly in a JSON * array of flows. */ public static class FlowEntryJsonArrayMatcher extends TypeSafeMatcher<JsonArray> { private final FlowEntry flow; private String reason = ""; public FlowEntryJsonArrayMatcher(FlowEntry flowValue) { flow = flowValue; } @Override public boolean matchesSafely(JsonArray json) { boolean flowFound = false; for (int jsonFlowIndex = 0; jsonFlowIndex < json.size(); jsonFlowIndex++) { final JsonObject jsonFlow = json.get(jsonFlowIndex).asObject(); final String flowId = Long.toString(flow.id().value()); final String jsonFlowId = jsonFlow.get("id").asString(); if (jsonFlowId.equals(flowId)) { flowFound = true; // We found the correct flow, check attribute values assertThat(jsonFlow, matchesFlow(flow, APP_ID.name())); } } if (!flowFound) { reason = "Flow with id " + flow.id().toString() + " not found"; return false; } else { return true; } } @Override public void describeTo(Description description) { description.appendText(reason); } } /** * Factory to allocate a flow array matcher. * * @param flow flow object we are looking for * @return matcher */ private static FlowEntryJsonArrayMatcher hasFlow(FlowEntry flow) { return new FlowEntryJsonArrayMatcher(flow); } /** * Hamcrest matcher to check that a flow representation in JSON matches * the actual flow rule. */ public static class FlowRuleJsonMatcher extends TypeSafeMatcher<JsonObject> { private final FlowRule flow; private final String expectedAppId; private String reason = ""; public FlowRuleJsonMatcher(FlowRule flowValue, String expectedAppIdValue) { flow = flowValue; expectedAppId = expectedAppIdValue; } @Override public boolean matchesSafely(JsonObject jsonFlow) { // check id final String jsonId = jsonFlow.get("id").asString(); final String flowId = Long.toString(flow.id().value()); if (!jsonId.equals(flowId)) { reason = "id " + flow.id().toString(); return false; } // check application id final String jsonAppId = jsonFlow.get("appId").asString(); if (!jsonAppId.equals(expectedAppId)) { reason = "appId " + Short.toString(flow.appId()); return false; } // check device id final String jsonDeviceId = jsonFlow.get("deviceId").asString(); if (!jsonDeviceId.equals(flow.deviceId().toString())) { reason = "deviceId " + flow.deviceId(); return false; } // check treatment and instructions array if (flow.treatment() != null) { final JsonObject jsonTreatment = jsonFlow.get("treatment").asObject(); final JsonArray jsonInstructions = jsonTreatment.get("instructions").asArray(); if (flow.treatment().immediate().size() != jsonInstructions.size()) { reason = "instructions array size of " + Integer.toString(flow.treatment().immediate().size()); return false; } for (final Instruction instruction : flow.treatment().immediate()) { boolean instructionFound = false; for (int instructionIndex = 0; instructionIndex < jsonInstructions.size(); instructionIndex++) { final String jsonType = jsonInstructions.get(instructionIndex) .asObject().get("type").asString(); final String instructionType = instruction.type().name(); if (jsonType.equals(instructionType)) { instructionFound = true; } } if (!instructionFound) { reason = "instruction " + instruction.toString(); return false; } } } // check selector and criteria array if (flow.selector() != null) { final JsonObject jsonTreatment = jsonFlow.get("selector").asObject(); final JsonArray jsonCriteria = jsonTreatment.get("criteria").asArray(); if (flow.selector().criteria().size() != jsonCriteria.size()) { reason = "criteria array size of " + Integer.toString(flow.selector().criteria().size()); return false; } for (final Criterion criterion : flow.selector().criteria()) { boolean criterionFound = false; for (int criterionIndex = 0; criterionIndex < jsonCriteria.size(); criterionIndex++) { final String jsonType = jsonCriteria.get(criterionIndex) .asObject().get("type").asString(); final String criterionType = criterion.type().name(); if (jsonType.equals(criterionType)) { criterionFound = true; } } if (!criterionFound) { reason = "criterion " + criterion.toString(); return false; } } } return true; } @Override public void describeTo(Description description) { description.appendText(reason); } } /** * Factory to allocate a flow matcher. * * @param flow flow rule object we are looking for * @return matcher */ private static FlowRuleJsonMatcher matchesFlowRule(FlowRule flow, String expectedAppName) { return new FlowRuleJsonMatcher(flow, expectedAppName); } /** * Hamcrest matcher to check that a flow is represented properly in a JSON * array of flow rules. */ public static class FlowRuleJsonArrayMatcher extends TypeSafeMatcher<JsonArray> { private final FlowRule flow; private String reason = ""; public FlowRuleJsonArrayMatcher(FlowRule flowValue) { flow = flowValue; } @Override public boolean matchesSafely(JsonArray json) { boolean flowFound = false; for (int jsonFlowIndex = 0; jsonFlowIndex < json.size(); jsonFlowIndex++) { final JsonObject jsonFlow = json.get(jsonFlowIndex).asObject(); final String flowId = Long.toString(flow.id().value()); final String jsonFlowId = jsonFlow.get("id").asString(); if (jsonFlowId.equals(flowId)) { flowFound = true; // We found the correct flow, check attribute values assertThat(jsonFlow, matchesFlowRule(flow, APP_ID.name())); } } if (!flowFound) { reason = "Flow with id " + flow.id().toString() + " not found"; return false; } else { return true; } } @Override public void describeTo(Description description) { description.appendText(reason); } } /** * Factory to allocate a flow array matcher. * * @param flow flow rule object we are looking for * @return matcher */ private static FlowRuleJsonArrayMatcher hasFlowRule(FlowRule flow) { return new FlowRuleJsonArrayMatcher(flow); } /** * Tests the result of the rest api GET when there are no flows. */ @Test public void testFlowsEmptyArray() { expect(mockFlowService.getFlowEntries(deviceId1)) .andReturn(null).anyTimes(); expect(mockFlowService.getFlowEntries(deviceId2)) .andReturn(null).anyTimes(); replay(mockFlowService); replay(mockDeviceService); final WebTarget wt = target(); final String response = wt.path("flows").request().get(String.class); assertThat(response, is("{\"flows\":[]}")); } /** * Tests the result of the rest api GET when there are active flows. */ @Test public void testFlowsPopulatedArray() { setupMockFlows(); replay(mockFlowService); replay(mockDeviceService); final WebTarget wt = target(); final String response = wt.path("flows").request().get(String.class); final JsonObject result = Json.parse(response).asObject(); assertThat(result, notNullValue()); assertThat(result.names(), hasSize(1)); assertThat(result.names().get(0), is("flows")); final JsonArray jsonFlows = result.get("flows").asArray(); assertThat(jsonFlows, notNullValue()); assertThat(jsonFlows, hasFlow(flow1)); assertThat(jsonFlows, hasFlow(flow2)); assertThat(jsonFlows, hasFlow(flow3)); assertThat(jsonFlows, hasFlow(flow4)); } /** * Tests the result of a rest api GET for a device. */ @Test public void testFlowsSingleDevice() { setupMockFlows(); final Set<FlowEntry> flows = new HashSet<>(); flows.add(flow5); flows.add(flow6); expect(mockFlowService.getFlowEntries(anyObject())) .andReturn(flows).anyTimes(); replay(mockFlowService); replay(mockDeviceService); final WebTarget wt = target(); final String response = wt.path("flows/" + deviceId3).request().get(String.class); final JsonObject result = Json.parse(response).asObject(); assertThat(result, notNullValue()); assertThat(result.names(), hasSize(1)); assertThat(result.names().get(0), is("flows")); final JsonArray jsonFlows = result.get("flows").asArray(); assertThat(jsonFlows, notNullValue()); assertThat(jsonFlows, hasFlow(flow5)); assertThat(jsonFlows, hasFlow(flow6)); } /** * Tests the result of a rest api GET for a device. */ @Test public void testFlowsSingleDeviceWithFlowId() { setupMockFlows(); final Set<FlowEntry> flows = new HashSet<>(); flows.add(flow5); flows.add(flow6); expect(mockFlowService.getFlowEntries(anyObject())) .andReturn(flows).anyTimes(); replay(mockFlowService); replay(mockDeviceService); final WebTarget wt = target(); final String response = wt.path("flows/" + deviceId3 + "/" + Long.toString(flow5.id().value())).request().get(String.class); final JsonObject result = Json.parse(response).asObject(); assertThat(result, notNullValue()); assertThat(result.names(), hasSize(1)); assertThat(result.names().get(0), is("flows")); final JsonArray jsonFlows = result.get("flows").asArray(); assertThat(jsonFlows, notNullValue()); assertThat(jsonFlows, hasFlow(flow5)); assertThat(jsonFlows, not(hasFlow(flow6))); } /** * Tests that a fetch of a non-existent device object throws an exception. */ @Test public void testBadGet() { expect(mockFlowService.getFlowEntries(anyObject())) .andReturn(null).anyTimes(); replay(mockFlowService); replay(mockDeviceService); WebTarget wt = target(); try { wt.path("flows/0").request().get(String.class); fail("Fetch of non-existent device did not throw an exception"); } catch (NotFoundException ex) { assertThat(ex.getMessage(), containsString("HTTP 404 Not Found")); } } /** * Tests creating a flow with POST. */ @Test public void testPostWithoutAppId() { mockFlowService.applyFlowRules(anyObject()); expectLastCall(); replay(mockFlowService); WebTarget wt = target(); InputStream jsonStream = FlowsResourceTest.class .getResourceAsStream("post-flow.json"); Response response = wt.path("flows/of:0000000000000001") .request(MediaType.APPLICATION_JSON_TYPE) .post(Entity.json(jsonStream)); assertThat(response.getStatus(), is(HttpURLConnection.HTTP_CREATED)); String location = response.getLocation().getPath(); assertThat(location, Matchers.startsWith("/flows/of:0000000000000001/")); } /** * Tests creating a flow with POST while specifying application identifier. */ @Test public void testPostWithAppId() { mockFlowService.applyFlowRules(anyObject()); expectLastCall(); replay(mockFlowService); WebTarget wt = target(); InputStream jsonStream = FlowsResourceTest.class .getResourceAsStream("post-flow.json"); Response response = wt.path("flows/of:0000000000000001") .queryParam("appId", "org.onosproject.rest") .request(MediaType.APPLICATION_JSON_TYPE) .post(Entity.json(jsonStream)); assertThat(response.getStatus(), is(HttpURLConnection.HTTP_CREATED)); String location = response.getLocation().getPath(); assertThat(location, Matchers.startsWith("/flows/of:0000000000000001/")); } /** * Tests deleting a flow. */ @Test public void testDelete() { setupMockFlows(); mockFlowService.removeFlowRules(anyObject()); expectLastCall(); replay(mockFlowService); WebTarget wt = target(); String location = "/flows/1/155"; Response deleteResponse = wt.path(location) .request(MediaType.APPLICATION_JSON_TYPE) .delete(); assertThat(deleteResponse.getStatus(), is(HttpURLConnection.HTTP_NO_CONTENT)); } /** * Tests the result of a rest api GET for an application. */ @Test public void testGetFlowByAppId() { setupMockFlows(); expect(mockApplicationService.getId(anyObject())).andReturn(APP_ID).anyTimes(); replay(mockApplicationService); expect(mockFlowService.getFlowEntriesById(APP_ID)).andReturn(flowEntries).anyTimes(); replay(mockFlowService); final WebTarget wt = target(); final String response = wt.path("/flows/application/1").request().get(String.class); final JsonObject result = Json.parse(response).asObject(); assertThat(result, notNullValue()); assertThat(result.names(), hasSize(1)); assertThat(result.names().get(0), is("flows")); final JsonArray jsonFlows = result.get("flows").asArray(); assertThat(jsonFlows, notNullValue()); assertThat(jsonFlows, hasFlowRule(flow1)); assertThat(jsonFlows, hasFlowRule(flow2)); assertThat(jsonFlows, hasFlowRule(flow3)); assertThat(jsonFlows, hasFlowRule(flow4)); } /** * Tests the result of a rest api DELETE for an application. */ @Test public void testRemoveFlowByAppId() { expect(mockApplicationService.getId(anyObject())).andReturn(APP_ID).anyTimes(); replay(mockApplicationService); mockFlowService.removeFlowRulesById(APP_ID); expectLastCall(); replay(mockFlowService); WebTarget wt = target(); String location = "/flows/application/1"; Response deleteResponse = wt.path(location) .request() .delete(); assertThat(deleteResponse.getStatus(), is(HttpURLConnection.HTTP_NO_CONTENT)); } }