package com.temenos.interaction.core.rim; /* * #%L * interaction-core * %% * Copyright (C) 2012 - 2013 Temenos Holdings N.V. * %% * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * #L% */ import static org.hamcrest.CoreMatchers.allOf; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.core.IsEqual.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyBoolean; import static org.mockito.Matchers.anyString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; import static org.powermock.api.mockito.PowerMockito.whenNew; import java.util.*; import javax.ws.rs.HttpMethod; import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.Response.StatusType; import javax.ws.rs.core.UriInfo; import com.temenos.interaction.core.command.*; import com.temenos.interaction.core.hypermedia.expression.ResourceGETExpression; import com.temenos.interaction.core.workflow.InteractionWorkflowStrategyCommandBuilder; import com.temenos.interaction.core.workflow.TransitionWorkflowStrategyCommandBuilder; import com.temenos.interaction.core.workflow.WorkflowCommandBuilderFactory; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import com.temenos.interaction.core.MultivaluedMapHelper; import com.temenos.interaction.core.MultivaluedMapImpl; import com.temenos.interaction.core.command.InteractionCommand.Result; import com.temenos.interaction.core.entity.Entity; import com.temenos.interaction.core.entity.EntityMetadata; import com.temenos.interaction.core.entity.EntityProperties; import com.temenos.interaction.core.entity.GenericError; import com.temenos.interaction.core.entity.Metadata; import com.temenos.interaction.core.hypermedia.Action; import com.temenos.interaction.core.hypermedia.BeanTransformer; import com.temenos.interaction.core.hypermedia.CollectionResourceState; import com.temenos.interaction.core.hypermedia.DynamicResourceState; import com.temenos.interaction.core.hypermedia.Link; import com.temenos.interaction.core.hypermedia.ResourceLocator; import com.temenos.interaction.core.hypermedia.ResourceLocatorProvider; import com.temenos.interaction.core.hypermedia.ResourceState; import com.temenos.interaction.core.hypermedia.ResourceStateMachine; import com.temenos.interaction.core.hypermedia.Transition; import com.temenos.interaction.core.resource.EntityResource; import com.temenos.interaction.core.resource.RESTResource; import com.temenos.interaction.core.resource.ResourceTypeHelper; import com.temenos.interaction.core.web.RequestContext; @RunWith(PowerMockRunner.class) @PrepareForTest({HTTPHypermediaRIM.class}) public class TestResponseHTTPHypermediaRIM { class MockEntity { String id; MockEntity(String id) { this.id = id; } public String getId() { return id; } }; @Before public void setup() { // initialise the thread local request context with requestUri and baseUri RequestContext ctx = new RequestContext("/baseuri", "/requesturi", null); RequestContext.setRequestContext(ctx); } private List<Action> mockActions() { return mockActions(new Action("GET", Action.TYPE.VIEW), new Action("DO", Action.TYPE.ENTRY)); } private List<Action> mockActions(Action...actions) { List<Action> actionsList = new ArrayList<Action>(); for (Action a : actions) { actionsList.add(a); } return actionsList; } private List<Action> mockIncrementAction() { List<Action> actions = new ArrayList<Action>(); actions.add(new Action("INCREMENT", Action.TYPE.ENTRY, null, "POST")); return actions; } private List<Action> mockSingleAction(String actionName, Action.TYPE type, String method){ List<Action> actions = new ArrayList<Action>(); actions.add(new Action(actionName, type, null, method)); return actions; } private List<Action> mockExceptionActions() { List<Action> actions = new ArrayList<Action>(); actions.add(new Action("GETException", Action.TYPE.VIEW)); return actions; } private List<Action> mockErrorActions() { List<Action> actions = new ArrayList<Action>(); actions.add(new Action("noop", Action.TYPE.VIEW)); return actions; } private Metadata createMockMetadata() { Metadata metadata = mock(Metadata.class); when(metadata.getEntityMetadata(any(String.class))).thenReturn(mock(EntityMetadata.class)); return metadata; } /* * This test checks that we receive a 404 'Not Found' if a GET command is not registered. * Every resource must have a GET command, so no command means no resource (404) */ @Test public void testGETCommandNotRegistered() { // our empty command controller CommandController mockCommandController = mock(CommandController.class); when(mockCommandController.fetchCommand("GET")).thenReturn(mock(InteractionCommand.class)); ResourceState initialState = new ResourceState("entity", "state", new ArrayList<Action>(), "/path"); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockCommandController, new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.get(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); } /* * This test checks that we receive a 405 'Method Not Allowed' if a command is * not registered for the given method PUT/POST/DELETE. */ @Test public void testPUTCommandNotRegisteredNotAllowedHeader() { // our empty command controller CommandController mockCommandController = mock(CommandController.class); when(mockCommandController.fetchCommand("GET")).thenReturn(mock(InteractionCommand.class)); ResourceState initialState = new ResourceState("entity", "state", new ArrayList<Action>(), "/path"); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockCommandController, new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.put(mock(HttpHeaders.class), "id", mockEmptyUriInfo(), mock(EntityResource.class)); assertEquals(HttpStatusTypes.METHOD_NOT_ALLOWED.getStatusCode(), response.getStatus()); // as per the http spec, 405 MUST include an Allow header List<Object> allowHeader = response.getMetadata().get("Allow"); assertNotNull(allowHeader); assertEquals(1, allowHeader.size()); HashSet<String> methodsAllowedDetected = new HashSet<String>(Arrays.asList(allowHeader.get(0).toString().split("\\s*,\\s*"))); HashSet<String> methodsAllowed = new HashSet<String>(Arrays.asList("GET, OPTIONS, HEAD".split("\\s*,\\s*"))); assertEquals(methodsAllowed, methodsAllowedDetected); } /* * This test checks that we receive a 405 'Method Not Allowed' if a command is * not registered for the given method PUT/POST/DELETE. */ @Test public void testPOSTCommandNotRegisteredNotAllowedHeader() { // our empty command controller CommandController mockCommandController = mock(CommandController.class); when(mockCommandController.fetchCommand("GET")).thenReturn(mock(InteractionCommand.class)); ResourceState initialState = new ResourceState("entity", "state", new ArrayList<Action>(), "/path"); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockCommandController, new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.post(mock(HttpHeaders.class), "id", mockEmptyUriInfo(), mock(EntityResource.class)); assertEquals(HttpStatusTypes.METHOD_NOT_ALLOWED.getStatusCode(), response.getStatus()); // as per the http spec, 405 MUST include an Allow header List<Object> allowHeader = response.getMetadata().get("Allow"); assertNotNull(allowHeader); assertEquals(1, allowHeader.size()); HashSet<String> methodsAllowedDetected = new HashSet<String>(Arrays.asList(allowHeader.get(0).toString().split("\\s*,\\s*"))); HashSet<String> methodsAllowed = new HashSet<String>(Arrays.asList("GET, OPTIONS, HEAD".split("\\s*,\\s*"))); assertEquals(methodsAllowed, methodsAllowedDetected); } /* * This test checks that we receive a 405 'Method Not Allowed' if a command is * not registered for the given method PUT/POST/DELETE. */ @Test public void testDELETECommandNotRegisteredNotAllowedHeader() { // our empty command controller CommandController mockCommandController = mock(CommandController.class); when(mockCommandController.fetchCommand("GET")).thenReturn(mock(InteractionCommand.class)); ResourceState initialState = new ResourceState("entity", "state", new ArrayList<Action>(), "/path"); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockCommandController, new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.delete(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); assertEquals(HttpStatusTypes.METHOD_NOT_ALLOWED.getStatusCode(), response.getStatus()); // as per the http spec, 405 MUST include an Allow header List<Object> allowHeader = response.getMetadata().get("Allow"); assertNotNull(allowHeader); assertEquals(1, allowHeader.size()); HashSet<String> methodsAllowedDetected = new HashSet<String>(Arrays.asList(allowHeader.get(0).toString().split("\\s*,\\s*"))); HashSet<String> methodsAllowed = new HashSet<String>(Arrays.asList("GET, OPTIONS, HEAD".split("\\s*,\\s*"))); assertEquals(methodsAllowed, methodsAllowedDetected); } /* * This test is for a PUT request that returns HttpStatus 204 "No Content" * A PUT command that does not return a new resource will inform the client * that there is no new information to display, continue with the current * view of this resource. */ @Test public void testPUTBuildResponseWith204NoContent() throws Exception { ResourceState initialState = new ResourceState("entity", "initialstate", mockActions(), "/path"); ResourceState updateState = new ResourceState("entity", "updatestate", mockActions(), "/path"); initialState.addTransition(new Transition.Builder().method(HttpMethod.PUT).target(updateState).build()); /* * construct an InteractionCommand that simply mocks the result of * storing a resource, with no updated resource for the user agent * to re-display */ InteractionCommand mockCommand = new InteractionCommand() { @Override public Result execute(InteractionContext ctx) { // this is how a command indicates No Content ctx.setResource(null); return Result.SUCCESS; } }; // create mock command controller CommandController mockCommandController = mock(CommandController.class); when(mockCommandController.fetchCommand("GET")).thenReturn(mock(InteractionCommand.class)); when(mockCommandController.isValidCommand("DO")).thenReturn(true); when(mockCommandController.fetchCommand("DO")).thenReturn(mockCommand); // RIM with command controller that issues our mock InteractionCommand HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockCommandController, new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.put(mock(HttpHeaders.class), "id", mockEmptyUriInfo(), mock(EntityResource.class)); // null resource for no content RESTResource resource = (RESTResource) response.getEntity(); assertNull(resource); // 204 http status for no content assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); } /* * This test is for a PUT request that does not returns a resource; it returns * HttpStatus 200 "OK" rather than 204 because the response has some links. */ @Test public void testPUTBuildResponseNot204NoContentTransitions() throws Exception { ResourceState initialState = new ResourceState("entity", "initialstate", mockActions(), "/path"); ResourceState updateState = new ResourceState("entity", "updatestate", mockActions(), "/path"); ResourceState otherState = new ResourceState("entity", "otherstate", mockActions(), "/path"); initialState.addTransition(new Transition.Builder().method(HttpMethod.PUT).target(updateState).build()); updateState.addTransition(new Transition.Builder().method(HttpMethod.GET).target(otherState).build()); /* * construct an InteractionCommand that simply mocks the result of * storing a resource, with no updated resource for the user agent * to re-display */ InteractionCommand mockCommand = new InteractionCommand() { @Override public Result execute(InteractionContext ctx) { // this is how a command indicates No Content ctx.setResource(null); return Result.SUCCESS; } }; // create mock command controller CommandController mockCommandController = mock(CommandController.class); when(mockCommandController.fetchCommand("GET")).thenReturn(mock(InteractionCommand.class)); when(mockCommandController.isValidCommand("DO")).thenReturn(true); when(mockCommandController.fetchCommand("DO")).thenReturn(mockCommand); // RIM with command controller that issues our mock InteractionCommand HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockCommandController, new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.put(mock(HttpHeaders.class), "id", mockEmptyUriInfo(), mock(EntityResource.class)); // null resource for no content RESTResource resource = (RESTResource) response.getEntity(); assertNull(resource); // 200 http status for no content assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); } /* * This test is for a DELETE request that returns HttpStatus 204 "No Content" * A successful DELETE command does not return a new resource; where a target state * is not found we'll inform the user agent that everything went OK, but there is * nothing more to display i.e. No Content. */ @SuppressWarnings({ "unchecked" }) @Test public void testDELETEBuildResponseWith204NoContentNoTransition() throws Exception { /* * construct an InteractionContext that simply mocks the result of * deleting a resource, with no updated resource for the user agent * to re-display */ ResourceState initialState = new ResourceState("entity", "initialstate", mockActions(), "/path"); ResourceState updateState = new ResourceState("entity", "updatestate", mockActions(), "/path"); initialState.addTransition(new Transition.Builder().method(HttpMethod.DELETE).target(updateState).build()); /* * construct an InteractionCommand that simply mocks the result of * storing a resource, with no updated resource for the user agent * to re-display */ InteractionCommand mockCommand = new InteractionCommand() { @Override public Result execute(InteractionContext ctx) { // this is how a command indicates No Content ctx.setResource(null); return Result.SUCCESS; } }; // create mock command controller CommandController mockCommandController = mock(CommandController.class); when(mockCommandController.fetchCommand("GET")).thenReturn(mock(InteractionCommand.class)); when(mockCommandController.isValidCommand("DO")).thenReturn(true); when(mockCommandController.fetchCommand("DO")).thenReturn(mockCommand); // RIM with command controller that issues our mock InteractionCommand HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockCommandController, new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.delete(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); // null resource for no content RESTResource resource = (RESTResource) response.getEntity(); assertNull(resource); // 204 http status for no content assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); } /* * This test is for a DELETE request that returns HttpStatus 204 "No Content" * A successful DELETE command does not return a new resource; where a target state * is a psuedo final state (effectively no target) we'll inform the user agent * that everything went OK, but there is nothing more to display i.e. No Content. */ @SuppressWarnings({ "unchecked" }) @Test public void testDELETEBuildResponseWith200WithContent() throws Exception { /* * construct an InteractionContext that simply mocks the result of * deleting a resource, with no updated resource for the user agent * to re-display */ ResourceState initialState = new ResourceState("entity", "state", mockActions(), "/path"); initialState.addTransition(new Transition.Builder().method(HttpMethod.DELETE).target(initialState).build()); InteractionContext testContext = new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), mock(MultivaluedMap.class), mock(MultivaluedMap.class), initialState, mock(Metadata.class)); testContext.setResource(null); // mock 'new InteractionContext()' in call to delete whenNew(InteractionContext.class).withParameterTypes(UriInfo.class, HttpHeaders.class, MultivaluedMap.class, MultivaluedMap.class, ResourceState.class, Metadata.class) .withArguments(any(UriInfo.class), any(HttpHeaders.class), any(MultivaluedMap.class), any(MultivaluedMap.class), any(ResourceState.class), any(Metadata.class)).thenReturn(testContext); // RIM with command controller that issues commands that always return SUCCESS HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.delete(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); // null resource RESTResource resource = (RESTResource) response.getEntity(); assertNull(resource); // 200 http status for Success with transition's content assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); } /* * This test is for a DELETE request that returns HttpStatus 205 "Reset Content" * A successful DELETE command does not return a new resource and should inform * the user agent to refresh the current view. */ @Test public void testBuildResponseWith205ContentReset() throws Exception { /* * construct an InteractionContext that simply mocks the result of * deleting a resource, with no updated resource for the user agent * to re-display */ ResourceState initialState = new ResourceState("entity", "state", mockActions(), "/path"); ResourceState deletedState = new ResourceState(initialState, "deleted", mockActions()); initialState.addTransition(new Transition.Builder().method("DELETE").target(deletedState).build()); deletedState.addTransition(new Transition.Builder().flags(Transition.REDIRECT).target(initialState).build()); // RIM with command controller that issues commands that always return SUCCESS HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.delete(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); // null resource RESTResource resource = (RESTResource) response.getEntity(); assertNull(resource); // 205 http status for Reset Content assertEquals(HttpStatusTypes.RESET_CONTENT.getStatusCode(), response.getStatus()); } /* * This test is for a DELETE request that returns HttpStatus 205 "Reset Content" * A successful DELETE command does not return a new resource and should inform * the user agent to refresh the current view if the target is the same as the source. */ @SuppressWarnings({ "unchecked" }) @Test public void testBuildResponseWith205ContentResetDifferentResource() throws Exception { /* * construct an InteractionContext that simply mocks the result of * deleting a resource, with no updated resource for the user agent * to re-display */ CollectionResourceState initialState = new CollectionResourceState("entity", "state", mockActions(), "/entities"); ResourceState existsState = new ResourceState(initialState, "exists", mockActions(), "/123"); ResourceState deletedState = new ResourceState(existsState, "deleted", mockActions()); initialState.addTransition(new Transition.Builder().flags(Transition.FOR_EACH).method("GET").target(existsState).build()); initialState.addTransition(new Transition.Builder().method("DELETE").target(deletedState).build()); existsState.addTransition(new Transition.Builder().method("DELETE").target(deletedState).build()); // the auto transition deletedState.addTransition(new Transition.Builder().flags(Transition.REDIRECT).target(initialState).build()); InteractionContext testContext = new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), mock(MultivaluedMap.class), mock(MultivaluedMap.class), initialState, mock(Metadata.class)); testContext.setResource(null); // mock 'new InteractionContext()' in call to delete whenNew(InteractionContext.class).withParameterTypes(UriInfo.class, HttpHeaders.class, MultivaluedMap.class, MultivaluedMap.class, ResourceState.class, Metadata.class) .withArguments(any(UriInfo.class), any(HttpHeaders.class), any(MultivaluedMap.class), any(MultivaluedMap.class), any(ResourceState.class), any(Metadata.class)).thenReturn(testContext); // RIM with command controller that issues commands that always return SUCCESS HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState), createMockMetadata()); Collection<ResourceInteractionModel> children = rim.getChildren(); // find the resource interaction model for the entity item HTTPHypermediaRIM itemRIM = null; for (ResourceInteractionModel r : children) { if (r.getResourcePath().equals("/entities/123")) { itemRIM = (HTTPHypermediaRIM) r; } } // mock the Link header HttpHeaders mockHeaders = mock(HttpHeaders.class); List<String> links = new ArrayList<String>(); links.add("</path>; rel=\"entity.state>DELETE>entity.deleted\""); when(mockHeaders.getRequestHeader("Link")).thenReturn(links); Response response = itemRIM.delete(mockHeaders, "id", mockEmptyUriInfo()); // null resource RESTResource resource = (RESTResource) response.getEntity(); assertNull(resource); // 205 http status for Reset Content assertEquals(HttpStatusTypes.RESET_CONTENT.getStatusCode(), response.getStatus()); } /* * This test is for a DELETE request that supplies a custom link relation * via the Link header. See (see rfc5988) * When a user agent follows a link it is able to supply the link relations * given to it by the server for that link. This provides the server with * some information about which link the client followed, and therefore * what state/links to show them next. */ @Test public void testBuildResponseWith303SeeOtherDELETESameEntity() throws Exception { /* * construct an InteractionContext that simply mocks the result of * deleting a resource, with no updated resource for the user agent * to re-display */ ResourceState initialState = new ResourceState("home", "initial", mockActions(), "/machines"); ResourceState existsState = new ResourceState("toaster", "exists", mockActions(), "/machines/toaster"); ResourceState cookingState = new ResourceState(existsState, "cooking", mockActions(), "/cooking"); ResourceState idleState = new ResourceState(cookingState, "idle", mockActions()); // view the toaster if it exists (could show time remaining if cooking) initialState.addTransition(new Transition.Builder().method("GET").target(existsState).build()); // start cooking the toast existsState.addTransition(new Transition.Builder().method("PUT").target(cookingState).build()); // stop the toast cooking cookingState.addTransition(new Transition.Builder().method("DELETE").target(idleState).build()); idleState.addTransition(new Transition.Builder().flags(Transition.REDIRECT).target(existsState).build()); // RIM with command controller that issues commands that always return SUCCESS HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState), createMockMetadata()); Collection<ResourceInteractionModel> children = rim.getChildren(); // find the resource interaction model for the 'cooking' state HTTPHypermediaRIM cookingStateRIM = null; for (ResourceInteractionModel r : children) { if (r.getResourcePath().equals("/machines/toaster/cooking")) { cookingStateRIM = (HTTPHypermediaRIM) r; } } // mock the Link header HttpHeaders mockHeaders = mock(HttpHeaders.class); List<String> links = new ArrayList<String>(); links.add("</path>; rel=\"toaster.cooking>toaster.idle\""); when(mockHeaders.getRequestHeader("Link")).thenReturn(links); Response response = cookingStateRIM.delete(mockHeaders, "id", mockEmptyUriInfo()); // null resource RESTResource resource = (RESTResource) response.getEntity(); assertNull(resource); // 303 "See Other" instructs user agent to fetch another resource as specified by the 'Location' header assertEquals(Status.SEE_OTHER.getStatusCode(), response.getStatus()); List<Object> locationHeader = response.getMetadata().get("Location"); assertNotNull(locationHeader); assertEquals(1, locationHeader.size()); assertEquals("/baseuri/machines/toaster", locationHeader.get(0)); } /* * This test is for a GET request to a resource that defines an auto transition; * we expect to receive 303 'See Other'. */ @Test public void testBuildResponseWith303SeeOtherGET() throws Exception { /* * construct an InteractionContext that simply mocks the result of * a GET to a resource, with an auto transition */ ResourceState initialState = new ResourceState("home", "initial", mockActions(), "/machines"); ResourceState existsState = new ResourceState("toaster", "exists", mockActions(), "/machines/toaster"); ResourceState existsElsewhereState = new ResourceState("toaster", "existsOther", mockActions(), "/machines/toaster2"); // view the toaster if it exists (could show time remaining if cooking) initialState.addTransition(new Transition.Builder().method("GET").target(existsState).build()); existsState.addTransition(new Transition.Builder().flags(Transition.REDIRECT).target(existsElsewhereState).build()); // RIM with command controller that issues commands that always return SUCCESS HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState), createMockMetadata()); Collection<ResourceInteractionModel> children = rim.getChildren(); // find the resource interaction model for the 'exists' state HTTPHypermediaRIM existsStateRIM = null; for (ResourceInteractionModel r : children) { if (r.getResourcePath().equals("/machines/toaster")) { existsStateRIM = (HTTPHypermediaRIM) r; } } // mock the Link header Response response = existsStateRIM.get(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); // null resource RESTResource resource = (RESTResource) response.getEntity(); assertNull(resource); // 303 "See Other" instructs user agent to fetch another resource as specified by the 'Location' header assertEquals(Status.SEE_OTHER.getStatusCode(), response.getStatus()); List<Object> locationHeader = response.getMetadata().get("Location"); assertNotNull(locationHeader); assertEquals(1, locationHeader.size()); assertEquals("/baseuri/machines/toaster2", locationHeader.get(0)); } /* * Same as testBuildResponseWith303SeeOtherGET with parameters */ @SuppressWarnings("unchecked") @Test public void testBuildResponseWith303SeeOtherGETWithParameters() throws Exception { /* * construct an InteractionContext that simply mocks the result of * a GET to a resource, with an auto transition */ ResourceState initialState = new ResourceState("home", "initial", mockActions(), "/machines"); ResourceState existsState = new ResourceState("toaster", "exists", mockActions(), "/machines/toaster"); ResourceState existsElsewhereState = new ResourceState("toaster", "existsOther", mockActions(), "/machines/toaster/{id}"); // view the toaster if it exists (could show time remaining if cooking) initialState.addTransition(new Transition.Builder().method("GET").target(existsState).build()); existsState.addTransition(new Transition.Builder().flags(Transition.REDIRECT).target(existsElsewhereState).build()); // RIM with command controller that issues commands that always return SUCCESS HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState), createMockMetadata()); Collection<ResourceInteractionModel> children = rim.getChildren(); // find the resource interaction model for the 'exists' state HTTPHypermediaRIM existsStateRIM = null; for (ResourceInteractionModel r : children) { if (r.getResourcePath().equals("/machines/toaster")) { existsStateRIM = (HTTPHypermediaRIM) r; } } // mock the Link header MultivaluedMap<String, String> mockPathParameters = new MultivaluedMapImpl<String>(); mockPathParameters.add("id", "2"); UriInfo uriInfo = mock(UriInfo.class); when(uriInfo.getPathParameters(anyBoolean())).thenReturn(mockPathParameters); when(uriInfo.getQueryParameters(false)).thenReturn(mock(MultivaluedMap.class)); Response response = existsStateRIM.get(mock(HttpHeaders.class), "id", uriInfo); // null resource RESTResource resource = (RESTResource) response.getEntity(); assertNull(resource); // 303 "See Other" instructs user agent to fetch another resource as specified by the 'Location' header assertEquals(Status.SEE_OTHER.getStatusCode(), response.getStatus()); List<Object> locationHeader = response.getMetadata().get("Location"); assertNotNull(locationHeader); assertEquals(1, locationHeader.size()); assertEquals("/baseuri/machines/toaster/2", locationHeader.get(0)); } /* * Same as testBuildResponseWith303SeeOtherGET with query parameters */ @Test public void testBuildResponseWith303SeeOtherGETWithQueryParameters() throws Exception { /* * construct an InteractionContext that simply mocks the result of * a GET to a resource, with an auto transition */ ResourceState initialState = new ResourceState("home", "initial", mockActions(), "/machines"); ResourceState existsState = new ResourceState("toaster", "exists", mockActions(), "/machines/toaster"); ResourceState existsElsewhereState = new ResourceState("toaster", "existsOther", mockActions(), "/machines/toaster/{id}"); // view the toaster if it exists (could show time remaining if cooking) initialState.addTransition(new Transition.Builder().method("GET").target(existsState).build()); Map<String,String> uriParameters = new HashMap<String,String>(); uriParameters.put("test", "{test}"); existsState.addTransition(new Transition.Builder() .flags(Transition.REDIRECT) .target(existsElsewhereState) .uriParameters(uriParameters) .build()); // RIM with command controller that issues commands that always return SUCCESS HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState), createMockMetadata()); Collection<ResourceInteractionModel> children = rim.getChildren(); // find the resource interaction model for the 'exists' state HTTPHypermediaRIM existsStateRIM = null; for (ResourceInteractionModel r : children) { if (r.getResourcePath().equals("/machines/toaster")) { existsStateRIM = (HTTPHypermediaRIM) r; } } // mock the Link header MultivaluedMap<String, String> mockPathParameters = new MultivaluedMapImpl<String>(); mockPathParameters.add("id", "2"); UriInfo uriInfo = mock(UriInfo.class); when(uriInfo.getPathParameters(anyBoolean())).thenReturn(mockPathParameters); MultivaluedMap<String, String> mockQueryParameters = new MultivaluedMapImpl<String>(); mockQueryParameters.add("test", "123"); when(uriInfo.getQueryParameters(false)).thenReturn(mockQueryParameters); Response response = existsStateRIM.get(mock(HttpHeaders.class), "id", uriInfo); // null resource RESTResource resource = (RESTResource) response.getEntity(); assertNull(resource); // 303 "See Other" instructs user agent to fetch another resource as specified by the 'Location' header assertEquals(Status.SEE_OTHER.getStatusCode(), response.getStatus()); List<Object> locationHeader = response.getMetadata().get("Location"); assertNotNull(locationHeader); assertEquals(1, locationHeader.size()); assertEquals("/baseuri/machines/toaster/2?test=123", locationHeader.get(0)); } /* * Same as testBuildResponseWith303SeeOtherGET with query parameters that replace a token */ @Test public void testBuildResponseWith303SeeOtherGETWithTokenReplaceQueryParameters() throws Exception { /* * construct an InteractionContext that simply mocks the result of * a GET to a resource, with an auto transition */ ResourceState initialState = new ResourceState("home", "initial", mockActions(), "/machines"); ResourceState existsState = new ResourceState("toaster", "exists", mockActions(), "/machines/toaster"); ResourceState existsElsewhereState = new ResourceState("toaster", "existsOther", mockActions(), "/machines/toaster/{id}"); // view the toaster if it exists (could show time remaining if cooking) initialState.addTransition(new Transition.Builder().method("GET").target(existsState).build()); existsState.addTransition(new Transition.Builder().flags(Transition.REDIRECT).target(existsElsewhereState).build()); // RIM with command controller that issues commands that always return SUCCESS HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState), createMockMetadata()); Collection<ResourceInteractionModel> children = rim.getChildren(); // find the resource interaction model for the 'exists' state HTTPHypermediaRIM existsStateRIM = null; for (ResourceInteractionModel r : children) { if (r.getResourcePath().equals("/machines/toaster")) { existsStateRIM = (HTTPHypermediaRIM) r; } } // mock the Link header UriInfo uriInfo = mock(UriInfo.class); when(uriInfo.getPathParameters(anyBoolean())).thenReturn(new MultivaluedMapImpl<String>()); MultivaluedMap<String, String> mockQueryParameters = new MultivaluedMapImpl<String>(); mockQueryParameters.add("id", "123"); when(uriInfo.getQueryParameters(false)).thenReturn(mockQueryParameters); Response response = existsStateRIM.get(mock(HttpHeaders.class), "id", uriInfo); // null resource RESTResource resource = (RESTResource) response.getEntity(); assertNull(resource); // 303 "See Other" instructs user agent to fetch another resource as specified by the 'Location' header assertEquals(Status.SEE_OTHER.getStatusCode(), response.getStatus()); List<Object> locationHeader = response.getMetadata().get("Location"); assertNotNull(locationHeader); assertEquals(1, locationHeader.size()); assertEquals("/baseuri/machines/toaster/123", locationHeader.get(0)); } /* * This test is for a POST request that creates a new resource. */ @Test public void testBuildResponseWith201Created() throws Exception { /* * construct an InteractionContext that simply mocks the result of * creating a resource */ ResourceState initialState = new ResourceState("home", "initial", mockActions(new Action("GET", Action.TYPE.VIEW)), "/machines"); ResourceState createPsuedoState = new ResourceState(initialState, "create", mockActions(new Action("POST", Action.TYPE.ENTRY))); ResourceState individualMachine = new ResourceState(initialState, "machine", mockActions(new Action("GET", Action.TYPE.VIEW)), "/{id}"); // create new machine initialState.addTransition(new Transition.Builder().method("POST").target(createPsuedoState).build()); // an auto transition to the new resource Map<String, String> uriLinkageMap = new HashMap<String, String>(); uriLinkageMap.put("id", "{id}"); createPsuedoState.addTransition(new Transition.Builder().flags(Transition.AUTO).target(individualMachine).uriParameters(uriLinkageMap).build()); MapBasedCommandController cc = new MapBasedCommandController(); cc.getCommandMap().put("GET", createTransitionCommand("entity", new Entity("entity", null), Result.SUCCESS)); cc.getCommandMap().put("POST", createCommand("entity", null, Result.CREATED)); // RIM with command controller that issues commands that always return CREATED HTTPHypermediaRIM rim = new HTTPHypermediaRIM(cc, new ResourceStateMachine(initialState, new BeanTransformer()), createMockMetadata()); Response response = rim.post(mock(HttpHeaders.class), "id", mockEmptyUriInfo(), mockEntityResourceWithId("123")); // null resource @SuppressWarnings("rawtypes") GenericEntity ge = (GenericEntity) response.getEntity(); assertNotNull(ge); RESTResource resource = (RESTResource) ge.getEntity(); assertNotNull(resource); /* * 201 "Created" informs the user agent that 'the request has been fulfilled and resulted * in a new resource being created'. It can be accessed by a GET to the resource specified * by the 'Location' header */ assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); List<Object> locationHeader = response.getMetadata().get("Location"); assertNotNull(locationHeader); assertEquals(1, locationHeader.size()); assertEquals("/baseuri/machines/123", locationHeader.get(0)); } /* * This test is for a POST request that creates a new resource, and returns * the links for the resource we auto transition to. */ @Test public void testPOSTwithAutoTransition() throws Exception { /* * construct an InteractionContext that simply mocks the result of * creating a resource */ ResourceState initialState = new ResourceState("home", "initial", mockActions(new Action("GET", Action.TYPE.VIEW)), "/machines"); ResourceState createPsuedoState = new ResourceState(initialState, "create", mockActions(new Action("POST", Action.TYPE.ENTRY))); ResourceState approvePsuedoState = new ResourceState(initialState, "approve", mockActions(new Action("POST", Action.TYPE.ENTRY))); ResourceState individualMachine = new ResourceState(initialState, "machine", mockActions(new Action("GET", Action.TYPE.VIEW)), "/{id}"); individualMachine.addTransition(new Transition.Builder().method("GET").target(initialState).build()); // create new machine initialState.addTransition(new Transition.Builder().method("POST").target(createPsuedoState).build()); // The state should transition from create -> approve -> machine; this tests multi state auto transitions Map<String, String> uriLinkageMap = new HashMap<String, String>(); uriLinkageMap.put("id", "{id}"); createPsuedoState.addTransition(new Transition.Builder().flags(Transition.AUTO).target(approvePsuedoState).uriParameters(uriLinkageMap).build()); approvePsuedoState.addTransition(new Transition.Builder().flags(Transition.AUTO).target(individualMachine).uriParameters(uriLinkageMap).build()); MapBasedCommandController cc = new MapBasedCommandController(); cc.getCommandMap().put("GET", createCommand("entity", new Entity("entity", null), Result.SUCCESS)); cc.getCommandMap().put("POST", createCommand("entity", null, Result.CREATED)); // RIM with command controller that issues commands that always return CREATED ResourceStateMachine resourceStateMachine = new ResourceStateMachine(initialState, new BeanTransformer()); resourceStateMachine.setWorkflowCommandBuilderProvider(new WorkflowCommandBuilderFactory(cc)); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(cc, resourceStateMachine, createMockMetadata()); Response response = rim.post(mock(HttpHeaders.class), "id", mockEmptyUriInfo(), mockEntityResourceWithId("123")); assertEquals(Status.CREATED.getStatusCode(), response.getStatus()); // null resource @SuppressWarnings("rawtypes") GenericEntity ge = (GenericEntity) response.getEntity(); assertNotNull(ge); RESTResource resource = (RESTResource) ge.getEntity(); assertNotNull(resource); /* * Assert the links in the response match the target resource */ EntityResource<?> createdResource = (EntityResource<?>) ((GenericEntity<?>)response.getEntity()).getEntity(); List<Link> links = new ArrayList<Link>(createdResource.getLinks()); assertEquals(2, links.size()); assertEquals("machine", links.get(0).getTitle()); assertEquals("/baseuri/machines/123", links.get(0).getHref()); assertEquals("initial", links.get(1).getTitle()); assertEquals("/baseuri/machines", links.get(1).getHref()); } /* * This test is for a POST request that creates a new resource, and has * an auto transition to a resource that does not exist */ @Test public void testPOSTwithAutoTransitionToNonExistentResource() throws Exception { /* * construct an InteractionContext that simply mocks the result of * creating a resource */ ResourceState initialState = new ResourceState("home", "initial", mockActions(), "/machines"); ResourceState createPsuedoState = new ResourceState(initialState, "create", mockActions()); ResourceState individualMachine = new ResourceState(initialState, "machine", mockActions(), "/{id}"); individualMachine.addTransition(new Transition.Builder().method("GET").target(initialState).build()); // create new machine initialState.addTransition(new Transition.Builder().method("POST").target(createPsuedoState).build()); // an auto transition to the new resource Map<String, String> uriLinkageMap = new HashMap<String, String>(); uriLinkageMap.put("id", "{id}"); createPsuedoState.addTransition(new Transition.Builder().flags(Transition.AUTO).target(individualMachine).uriParameters(uriLinkageMap).build()); // RIM with command controller that issues commands that return SUCCESS for 'DO' action and FAILURE for 'GET' action (see mockActions()) Map<String,InteractionCommand> commands = new HashMap<String,InteractionCommand>(); commands.put("DO", mockCommand_SUCCESS()); commands.put("GET", mockTransitionCommand_FAILURE()); CommandController commandController = new MapBasedCommandController(commands); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(commandController, new ResourceStateMachine(initialState, new BeanTransformer()), createMockMetadata()); Response response = rim.post(mock(HttpHeaders.class), "id", mockEmptyUriInfo(), mockEntityResourceWithId("123")); assertEquals(Status.OK.getStatusCode(), response.getStatus()); // null resource @SuppressWarnings("rawtypes") GenericEntity ge = (GenericEntity) response.getEntity(); assertNotNull(ge); RESTResource resource = (RESTResource) ge.getEntity(); assertNotNull(resource); } @Test public void testPOSTWithTwoAutomaticTransitionsAndDynamicResourceStates() throws InteractionException { //given {a graph of resource states comprising of an initial state, //a middle state and a dynamic state that autotransitions to another resource state} ResourceState entrance = new ResourceState("house", "entrance", mockSingleAction("DO", Action.TYPE.ENTRY, "POST"), "/entrance", new String[]{"http://temenostech.temenos.com/rels/input"}); ResourceState doorframe1 = new ResourceState("house", "doorframe1", mockSingleAction("NEXT", Action.TYPE.VIEW, "GET"), "/doorframe1", new String[]{"http://temenostech.temenos.com/rels/next"}); ResourceState doorframe2 = new ResourceState("house", "doorframe2", mockSingleAction("NEXT", Action.TYPE.VIEW, "GET"), "/doorframe2", new String[]{"http://temenostech.temenos.com/rels/next"}); ResourceState door = new DynamicResourceState("house", "door", "locator", new String[]{"time"}); ResourceState hallway = new ResourceState("house", "hallway", mockIncrementAction(), "/hallway", (String[])null); ResourceState kitchen = new ResourceState("house", "kitchen", mockSingleAction("DONE", Action.TYPE.ENTRY, "POST"), "/kitchen", new String[]{"http://temenostech.temenos.com/rels/new"}); ResourceState sink = new ResourceState("house", "sink", mockActions(new Action("GET", Action.TYPE.VIEW)), "/sink"); ResourceState lighting = new ResourceState("house", "lighting", mockSingleAction("SEE", Action.TYPE.ENTRY, "POST"), "/lighting"); entrance.addTransition(new Transition.Builder().flags(Transition.AUTO).target(doorframe1).build()); doorframe1.addTransition(new Transition.Builder().flags(Transition.AUTO).target(door).build()); hallway.addTransition(new Transition.Builder().flags(Transition.AUTO).target(doorframe2).build()); doorframe2.addTransition(new Transition.Builder().flags(Transition.AUTO).target(door).build()); kitchen.addTransition(new Transition.Builder().method("GET").flags(Transition.EMBEDDED).target(lighting).build()); kitchen.addTransition(new Transition.Builder().method("GET").target(sink).build()); //and {a locator that always returns the hallway resource state when invoked with //string "day" and the kitchen resource state when invoked with string "night"} ResourceLocatorProvider locatorProvider = mock(ResourceLocatorProvider.class); ResourceLocator locator = mock(ResourceLocator.class); when(locator.resolve(eq("day"))).thenReturn(hallway); when(locator.resolve(eq("night"))).thenReturn(kitchen); when(locatorProvider.get(eq("locator"))).thenReturn(locator); Map<String, InteractionCommand> commands = new HashMap<String, InteractionCommand>(); InteractionCommand doSomething = mock(InteractionCommand.class), getSomething = mock(TransitionCommand.class), increment = mock(TransitionCommand.class), next = mock(TransitionCommand.class), see = mock(TransitionCommand.class), done = mock(TransitionCommand.class); //and {two InteractionCommands that execute successfully} when(getSomething.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); when(done.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); //and {a InteractionCommand for embedding content that fails} when(see.execute(any(InteractionContext.class))).thenReturn(Result.INVALID_REQUEST); //and {an InteractionCommand that sets up an alias for resolving a dynamic resource state} doAnswer(new Answer<Result>() { @Override public Result answer(InvocationOnMock invocationOnMock) throws Throwable { ((InteractionContext)invocationOnMock.getArguments()[0]).setAttribute("time", "day"); return Result.SUCCESS; } }).when(doSomething).execute(any(InteractionContext.class)); //and {an InteractionCommand that forwards any incoming query parameters} doAnswer(new Answer<Result>() { @Override public Result answer(InvocationOnMock invocationOnMock) throws Throwable { InteractionContext ctx = (InteractionContext)invocationOnMock.getArguments()[0]; if(ctx.getResource() == null){ ctx.setResource(new EntityResource<Object>()); } MultivaluedMapHelper.merge(ctx.getQueryParameters(), ctx.getOutQueryParameters(), MultivaluedMapHelper.Strategy.FAVOUR_DEST); return Result.SUCCESS; } }).when(next).execute(any(InteractionContext.class)); //and {an InteractionCommand that sets up an alias for resolving another //dynamic resource state and adds query parameters} doAnswer(new Answer<Result>() { @Override public Result answer(InvocationOnMock invocationOnMock) throws Throwable { InteractionContext ctx = (InteractionContext)invocationOnMock.getArguments()[0]; if(ctx.getOutQueryParameters().get("mode") != null){ ctx.setAttribute("time", "night"); ctx.getOutQueryParameters().add("mode", "run"); ctx.getOutQueryParameters().put("mode", new ArrayList<String>( Arrays.asList(new String[]{"run"})) ); } return Result.SUCCESS; } }).when(increment).execute(any(InteractionContext.class)); commands.put("GET", getSomething); commands.put("DO", doSomething); commands.put("DONE", done); commands.put("INCREMENT", increment); commands.put("NEXT", next); commands.put("SEE", see); CommandController commandController = new MapBasedCommandController(commands); //when {the post method is invoked} HTTPHypermediaRIM rim = new HTTPHypermediaRIM(commandController, new ResourceStateMachine(entrance, new BeanTransformer(), locatorProvider), createMockMetadata(), locatorProvider); Response response = rim.post(mock(HttpHeaders.class), "id", mockUriInfoWithParams(), mockEntityResourceWithId("123")); //then {the response must be HTTP 201 Created} assertEquals(Status.OK.getStatusCode(), response.getStatus()); //and {the response entity must not be null and must be an instance of/subtype RESTResource} assertNotNull(GenericEntity.class.isAssignableFrom(response.getEntity().getClass())); GenericEntity<?> responseEntity = (GenericEntity<?>) response.getEntity(); assertTrue(RESTResource.class.isAssignableFrom(responseEntity.getEntity().getClass())); //and {the response entity must contain a link with a query parameter added by a command appended to it} List<Link> links = new ArrayList<Link>(((RESTResource)responseEntity.getEntity()).getLinks()); assertThat(links.size(), equalTo(3)); assertThat(links.get(1).getHref(), equalTo("/baseuri/lighting?mode=run")); //and {the location header must be set to the final resource resolved //in the sequence of autotransitions} assertThat((String)response.getMetadata().get("Location").get(0), allOf(containsString("/baseuri/kitchen"), containsString("mode=run"))); verify(doSomething, times(1)).execute(any(InteractionContext.class)); verify(increment, times(1)).execute(any(InteractionContext.class)); verify(done, times(1)).execute(any(InteractionContext.class)); } @Test public void testPOSTTwoAutomaticTransitionsAndFailingMiddleResource() throws InteractionException{ //given {a graph of resource states comprising of an initial state, //a middle state and a dynamic state that autotransitions to another resource state} ResourceState entrance = new ResourceState("house", "entrance", mockSingleAction("DO", Action.TYPE.ENTRY, "POST"), "/entrance", new String[]{"http://temenostech.temenos.com/rels/input"}); ResourceState doorframe = new ResourceState("house", "doorframe", mockSingleAction("NEXT", Action.TYPE.VIEW, "GET"), "/doorframe", new String[]{"http://temenostech.temenos.com/rels/next"}); ResourceState door = new DynamicResourceState("house", "door", "locator", new String[]{"time"}); ResourceState hallway = new ResourceState("house", "hallway", mockIncrementAction(), "/hallway", (String[])null); ResourceState kitchen = new ResourceState("house", "kitchen", mockSingleAction("DONE", Action.TYPE.ENTRY, "POST"), "/kitchen", new String[]{"http://temenostech.temenos.com/rels/new"}); ResourceState sink = new ResourceState("house", "sink", mockSingleAction("GET", Action.TYPE.VIEW, "GET"), "/sink"); ResourceState lighting = new ResourceState("house", "lighting", mockSingleAction("SEE", Action.TYPE.ENTRY, "POST"), "/lighting"); entrance.addTransition(new Transition.Builder().flags(Transition.AUTO).target(doorframe).build()); doorframe.addTransition(new Transition.Builder().flags(Transition.AUTO).target(door).build()); hallway.addTransition(new Transition.Builder().flags(Transition.AUTO).target(doorframe).build()); kitchen.addTransition(new Transition.Builder().method("GET").flags(Transition.EMBEDDED).target(lighting).build()); kitchen.addTransition(new Transition.Builder().method("GET").target(sink).build()); //and {a locator that always returns the hallway resource state when invoked with //string "day" and the kitchen resource state when invoked with string "night"} ResourceLocatorProvider locatorProvider = mock(ResourceLocatorProvider.class); ResourceLocator locator = mock(ResourceLocator.class); when(locator.resolve(eq("day"))).thenReturn(hallway); when(locator.resolve(eq("night"))).thenReturn(kitchen); when(locatorProvider.get(eq("locator"))).thenReturn(locator); Map<String, InteractionCommand> commands = new HashMap<String, InteractionCommand>(); InteractionCommand doSomething = mock(InteractionCommand.class), getSomething = mock(TransitionCommand.class), increment = mock(TransitionCommand.class), next = mock(TransitionCommand.class), see = mock(TransitionCommand.class), done = mock(TransitionCommand.class); //and {two InteractionCommands that execute successfully} when(getSomething.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); when(done.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); //and {an InteractionCommand for embedding content that fails} when(see.execute(any(InteractionContext.class))).thenReturn(Result.INVALID_REQUEST); //and {middle InteractionCommand fails} when(increment.execute(any(InteractionContext.class))).thenThrow(new InteractionException(Status.BAD_REQUEST)); //and {an InteractionCommand that sets up an alias for resolving a dynamic resource state} doAnswer(new Answer<Result>() { @Override public Result answer(InvocationOnMock invocationOnMock) throws Throwable { ((InteractionContext)invocationOnMock.getArguments()[0]).setAttribute("time", "day"); return Result.SUCCESS; } }).when(doSomething).execute(any(InteractionContext.class)); //and {an InteractionCommand that forwards any incoming query parameters} doAnswer(new Answer<Result>() { @Override public Result answer(InvocationOnMock invocationOnMock) throws Throwable { InteractionContext ctx = (InteractionContext)invocationOnMock.getArguments()[0]; if(ctx.getResource() == null){ ctx.setResource(new EntityResource<Object>()); } MultivaluedMapHelper.merge(ctx.getQueryParameters(), ctx.getOutQueryParameters(), MultivaluedMapHelper.Strategy.FAVOUR_DEST); return Result.SUCCESS; } }).when(next).execute(any(InteractionContext.class)); commands.put("GET", getSomething); commands.put("DO", doSomething); commands.put("DONE", done); commands.put("INCREMENT", increment); commands.put("NEXT", next); commands.put("SEE", see); CommandController commandController = new MapBasedCommandController(commands); //when {the post method is invoked} HTTPHypermediaRIM rim = new HTTPHypermediaRIM(commandController, new ResourceStateMachine(entrance, new BeanTransformer(), locatorProvider), createMockMetadata(), locatorProvider); Response response = rim.post(mock(HttpHeaders.class), "id", mockUriInfoWithParams(), mockEntityResourceWithId("123")); //then {the response must be HTTP 200 ok} assertEquals(Status.OK.getStatusCode(), response.getStatus()); //and {the response entity must not be null and must be an instance of/subtype RESTResource} assertNotNull(GenericEntity.class.isAssignableFrom(response.getEntity().getClass())); GenericEntity<?> responseEntity = (GenericEntity<?>) response.getEntity(); assertTrue(RESTResource.class.isAssignableFrom(responseEntity.getEntity().getClass())); //and {the response entity must contain a link with a query parameter added by a command appended to it} List<Link> links = new ArrayList<Link>(((RESTResource)responseEntity.getEntity()).getLinks()); assertThat(links.size(), equalTo(1)); //and {the location header must be set to the final resource state resolved //in the sequence of successful autotransitions} assertThat((String)response.getMetadata().get("Location").get(0), allOf(containsString("/baseuri/doorframe"), containsString("mode=walk"))); verify(doSomething, times(1)).execute(any(InteractionContext.class)); verify(increment, times(1)).execute(any(InteractionContext.class)); verify(done, times(0)).execute(any(InteractionContext.class)); } @Test public void testPOSTWithTwoAutomaticTransitionsAndDynamicResourceState() throws InteractionException { //given {a graph of resource states comprising of an initial state //and a dynamic state that autotransitions to another resource state} ResourceState entrance = new ResourceState( "house", "entrance", mockSingleAction("DO", Action.TYPE.ENTRY, "POST"), "/entrance", new String[]{"http://temenostech.temenos.com/rels/input"} ); ResourceState doorframe = new ResourceState( "house", "doorframe", mockSingleAction("NEXT", Action.TYPE.VIEW, "GET"), "/doorframe", new String[]{"http://temenostech.temenos.com/rels/next"} ); ResourceState door = new DynamicResourceState("house", "door", "locator", new String[]{"time"}); ResourceState hallway = new ResourceState( "house", "hallway", mockSingleAction("DONE", Action.TYPE.ENTRY, "POST"), "/hallway", (String[])null ); ResourceState curtains = new ResourceState("house", "curtains", mockSingleAction("GET", Action.TYPE.VIEW, "GET"), "/curtains"); ResourceState lighting = new ResourceState("house", "lighting", mockSingleAction("GET", Action.TYPE.VIEW, "GET"), "/lighting"); entrance.addTransition(new Transition.Builder().flags(Transition.AUTO).target(doorframe).build()); doorframe.addTransition(new Transition.Builder().flags(Transition.AUTO).target(door).build()); hallway.addTransition(new Transition.Builder().method("GET").target(curtains).build()); hallway.addTransition(new Transition.Builder().method("GET").target(lighting).build()); //and {a locator that always returns the hallway resource state when invoked with //string "day"} ResourceLocatorProvider locatorProvider = mock(ResourceLocatorProvider.class); ResourceLocator locator = mock(ResourceLocator.class); when(locator.resolve(eq("day"))).thenReturn(hallway); when(locatorProvider.get(eq("locator"))).thenReturn(locator); Map<String, InteractionCommand> commands = new HashMap<>(); InteractionCommand doSomething = mock(InteractionCommand.class); TransitionCommand getSomething = mock(TransitionCommand.class), next = mock(TransitionCommand.class), see = mock(TransitionCommand.class), done = mock(TransitionCommand.class); //and {three InteractionCommands that execute successfully} when(getSomething.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); when(done.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); when(next.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); //and {an InteractionCommand that sets up an alias for resolving a //dynamic resource state and adds query parameters} doAnswer(new Answer<Result>() { @Override public Result answer(InvocationOnMock invocationOnMock) throws Throwable { InteractionContext ctx = ((InteractionContext)invocationOnMock.getArguments()[0]); ctx.setAttribute("time", "day"); ctx.getOutQueryParameters().put("mode", new ArrayList<String>( Arrays.asList(new String[]{"run"})) ); return Result.SUCCESS; } }).when(doSomething).execute(any(InteractionContext.class)); commands.put("GET", getSomething); commands.put("DO", doSomething); commands.put("DONE", done); commands.put("NEXT", next); commands.put("SEE", see); CommandController commandController = new MapBasedCommandController(commands); //when {the post method is invoked} HTTPHypermediaRIM rim = new HTTPHypermediaRIM(commandController, new ResourceStateMachine( entrance, new BeanTransformer(), locatorProvider ), createMockMetadata(), locatorProvider ); Response response = rim.post(mock(HttpHeaders.class), "id", mockUriInfoWithParams(), mockEntityResourceWithId("123")); //then {the response must be HTTP 201 Created} assertEquals(Status.OK.getStatusCode(), response.getStatus()); //and {the response entity must not be null and must be an instance of/subtype RESTResource} assertNotNull(GenericEntity.class.isAssignableFrom(response.getEntity().getClass())); GenericEntity<?> responseEntity = (GenericEntity<?>) response.getEntity(); assertTrue(RESTResource.class.isAssignableFrom(responseEntity.getEntity().getClass())); //and {the response entity must contain a link with a query parameter added by a command appended to it} List<Link> links = new ArrayList<Link>(((RESTResource)responseEntity.getEntity()).getLinks()); assertThat(links.size(), equalTo(3)); assertThat(links.get(1).getHref(), equalTo("/baseuri/curtains?mode=run")); //and {the location header must be set to the final resource resolved //in the sequence of autotransitions} assertThat((String)response.getMetadata().get("Location").get(0), allOf(containsString("/baseuri/hallway"), containsString("mode=run"))); verify(doSomething, times(1)).execute(any(InteractionContext.class)); verify(done, times(1)).execute(any(InteractionContext.class)); } @Test public void testPOSTWithTwoConditionalAutomaticTransitionsAndDynamicResourceStates() throws InteractionException { //given {a graph of resource states comprising of an initial state, //a middle state and a dynamic state that autotransitions to another resource state} ResourceState entrance = new ResourceState("house", "entrance", mockSingleAction("DO", Action.TYPE.ENTRY, "POST"), "/entrance", new String[]{"http://temenostech.temenos.com/rels/input"}); ResourceState doorframe = new ResourceState("house", "doorframe", mockSingleAction("NEXT", Action.TYPE.VIEW, "GET"), "/doorframe", new String[]{"http://temenostech.temenos.com/rels/next"}); ResourceState door = new DynamicResourceState("house", "door", "locator", new String[]{"time"}); ResourceState hallway = new ResourceState("house", "hallway", mockIncrementAction(), "/hallway", (String[])null); ResourceState kitchen = new ResourceState("house", "kitchen", mockSingleAction("DONE", Action.TYPE.ENTRY, "POST"), "/kitchen", new String[]{"http://temenostech.temenos.com/rels/new"}); ResourceState sink = new ResourceState("house", "sink", mockActions(new Action("GET", Action.TYPE.VIEW)), "/sink"); ResourceState lighting = new ResourceState("house", "lighting", mockSingleAction("SEE", Action.TYPE.ENTRY, "POST"), "/lighting"); entrance.addTransition(new Transition.Builder().flags(Transition.AUTO).target(doorframe).evaluation(new ResourceGETExpression(doorframe, ResourceGETExpression.Function.OK)).build()); doorframe.addTransition(new Transition.Builder().flags(Transition.AUTO).target(door).build()); hallway.addTransition(new Transition.Builder().flags(Transition.AUTO).target(doorframe).evaluation(new ResourceGETExpression(doorframe, ResourceGETExpression.Function.OK)).build()); kitchen.addTransition(new Transition.Builder().method("GET").flags(Transition.EMBEDDED).target(lighting).build()); kitchen.addTransition(new Transition.Builder().method("GET").target(sink).build()); //and {a locator that always returns the hallway resource state when invoked with //string "day" and the kitchen resource state when invoked with string "night"} ResourceLocatorProvider locatorProvider = mock(ResourceLocatorProvider.class); ResourceLocator locator = mock(ResourceLocator.class); when(locator.resolve(eq("day"))).thenReturn(hallway); when(locator.resolve(eq("night"))).thenReturn(kitchen); when(locatorProvider.get(eq("locator"))).thenReturn(locator); Map<String, InteractionCommand> commands = new HashMap<String, InteractionCommand>(); InteractionCommand doSomething = mock(InteractionCommand.class), getSomething = mock(InteractionCommand.class), increment = mock(InteractionCommand.class), next = mock(InteractionCommand.class), see = mock(InteractionCommand.class), done = mock(InteractionCommand.class); //and {two InteractionCommands that execute successfully} when(getSomething.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); when(done.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); //and {a InteractionCommand for embedding content that fails} when(see.execute(any(InteractionContext.class))).thenReturn(Result.INVALID_REQUEST); //and {an InteractionCommand that sets up an alias for resolving a dynamic resource state} doAnswer(new Answer<Result>() { @Override public Result answer(InvocationOnMock invocationOnMock) throws Throwable { ((InteractionContext)invocationOnMock.getArguments()[0]).setAttribute("time", "day"); return Result.SUCCESS; } }).when(doSomething).execute(any(InteractionContext.class)); //and {an InteractionCommand that forwards any incoming query parameters} doAnswer(new Answer<Result>() { @Override public Result answer(InvocationOnMock invocationOnMock) throws Throwable { InteractionContext ctx = (InteractionContext)invocationOnMock.getArguments()[0]; if(ctx.getResource() == null){ ctx.setResource(new EntityResource<Object>()); } MultivaluedMapHelper.merge(ctx.getQueryParameters(), ctx.getOutQueryParameters(), MultivaluedMapHelper.Strategy.FAVOUR_DEST); return Result.SUCCESS; } }).when(next).execute(any(InteractionContext.class)); //and {an InteractionCommand that sets up an alias for resolving another //dynamic resource state and adds query parameters} doAnswer(new Answer<Result>() { @Override public Result answer(InvocationOnMock invocationOnMock) throws Throwable { InteractionContext ctx = (InteractionContext)invocationOnMock.getArguments()[0]; if(ctx.getOutQueryParameters().get("mode") != null){ ctx.setAttribute("time", "night"); ctx.getOutQueryParameters().add("mode", "run"); ctx.getOutQueryParameters().put("mode", new ArrayList<String>( Arrays.asList(new String[]{"run"})) ); } return Result.SUCCESS; } }).when(increment).execute(any(InteractionContext.class)); commands.put("GET", getSomething); commands.put("DO", doSomething); commands.put("DONE", done); commands.put("INCREMENT", increment); commands.put("NEXT", next); commands.put("SEE", see); CommandController commandController = new MapBasedCommandController(commands); //when {the post method is invoked} HTTPHypermediaRIM rim = new HTTPHypermediaRIM(commandController, new ResourceStateMachine(entrance, new BeanTransformer(), locatorProvider), createMockMetadata(), locatorProvider); Response response = rim.post(mock(HttpHeaders.class), "id", mockUriInfoWithParams(), mockEntityResourceWithId("123")); //then {the response must be HTTP 201 Created} assertEquals(Status.OK.getStatusCode(), response.getStatus()); //and {the response entity must not be null and must be an instance of/subtype RESTResource} assertNotNull(GenericEntity.class.isAssignableFrom(response.getEntity().getClass())); GenericEntity<?> responseEntity = (GenericEntity<?>) response.getEntity(); assertTrue(RESTResource.class.isAssignableFrom(responseEntity.getEntity().getClass())); //and {the response entity must contain a link with a query parameter added by a command appended to it} List<Link> links = new ArrayList<Link>(((RESTResource)responseEntity.getEntity()).getLinks()); assertThat(links.size(), equalTo(3)); assertThat(links.get(1).getHref(), equalTo("/baseuri/lighting?mode=run")); //and {the location header must be set to the final resource resolved //in the sequence of autotransitions} assertThat((String)response.getMetadata().get("Location").get(0), allOf(containsString("/baseuri/kitchen"), containsString("mode=run"))); verify(doSomething, times(1)).execute(any(InteractionContext.class)); verify(increment, times(1)).execute(any(InteractionContext.class)); verify(done, times(1)).execute(any(InteractionContext.class)); } @Test public void testPOSTTwoConditionalAutomaticTransitionsAndFailingMiddleResource() throws InteractionException{ //given {a graph of resource states comprising of an initial state, //a middle state and a dynamic state that autotransitions to another resource state} ResourceState entrance = new ResourceState("house", "entrance", mockSingleAction("DO", Action.TYPE.ENTRY, "POST"), "/entrance", new String[]{"http://temenostech.temenos.com/rels/input"}); ResourceState doorframe = new ResourceState("house", "doorframe", mockSingleAction("NEXT", Action.TYPE.VIEW, "GET"), "/doorframe", new String[]{"http://temenostech.temenos.com/rels/next"}); ResourceState door = new DynamicResourceState("house", "door", "locator", new String[]{"time"}); ResourceState hallway = new ResourceState("house", "hallway", mockIncrementAction(), "/hallway", (String[])null); ResourceState kitchen = new ResourceState("house", "kitchen", mockSingleAction("DONE", Action.TYPE.ENTRY, "POST"), "/kitchen", new String[]{"http://temenostech.temenos.com/rels/new"}); ResourceState sink = new ResourceState("house", "sink", mockSingleAction("GET", Action.TYPE.VIEW, "GET"), "/sink"); ResourceState lighting = new ResourceState("house", "lighting", mockSingleAction("SEE", Action.TYPE.ENTRY, "POST"), "/lighting"); entrance.addTransition(new Transition.Builder().flags(Transition.AUTO).target(doorframe).evaluation(new ResourceGETExpression(doorframe, ResourceGETExpression.Function.OK)).build()); doorframe.addTransition(new Transition.Builder().flags(Transition.AUTO).target(door).build()); hallway.addTransition(new Transition.Builder().flags(Transition.AUTO).target(doorframe).evaluation(new ResourceGETExpression(doorframe, ResourceGETExpression.Function.OK)).build()); kitchen.addTransition(new Transition.Builder().method("GET").flags(Transition.EMBEDDED).target(lighting).build()); kitchen.addTransition(new Transition.Builder().method("GET").target(sink).build()); //and {a locator that always returns the hallway resource state when invoked with //string "day" and the kitchen resource state when invoked with string "night"} ResourceLocatorProvider locatorProvider = mock(ResourceLocatorProvider.class); ResourceLocator locator = mock(ResourceLocator.class); when(locator.resolve(eq("day"))).thenReturn(hallway); when(locator.resolve(eq("night"))).thenReturn(kitchen); when(locatorProvider.get(eq("locator"))).thenReturn(locator); Map<String, InteractionCommand> commands = new HashMap<String, InteractionCommand>(); InteractionCommand doSomething = mock(InteractionCommand.class), getSomething = mock(InteractionCommand.class), increment = mock(InteractionCommand.class), next = mock(InteractionCommand.class), see = mock(InteractionCommand.class), done = mock(InteractionCommand.class); //and {two InteractionCommands that execute successfully} when(getSomething.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); when(done.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); //and {an InteractionCommand for embedding content that fails} when(see.execute(any(InteractionContext.class))).thenReturn(Result.INVALID_REQUEST); //and {middle InteractionCommand fails} when(increment.execute(any(InteractionContext.class))).thenThrow(new InteractionException(Status.BAD_REQUEST)); //and {an InteractionCommand that sets up an alias for resolving a dynamic resource state} doAnswer(new Answer<Result>() { @Override public Result answer(InvocationOnMock invocationOnMock) throws Throwable { ((InteractionContext)invocationOnMock.getArguments()[0]).setAttribute("time", "day"); return Result.SUCCESS; } }).when(doSomething).execute(any(InteractionContext.class)); //and {an InteractionCommand that forwards any incoming query parameters} doAnswer(new Answer<Result>() { @Override public Result answer(InvocationOnMock invocationOnMock) throws Throwable { InteractionContext ctx = (InteractionContext)invocationOnMock.getArguments()[0]; if(ctx.getResource() == null){ ctx.setResource(new EntityResource<Object>()); } MultivaluedMapHelper.merge(ctx.getQueryParameters(), ctx.getOutQueryParameters(), MultivaluedMapHelper.Strategy.FAVOUR_DEST); return Result.SUCCESS; } }).when(next).execute(any(InteractionContext.class)); commands.put("GET", getSomething); commands.put("DO", doSomething); commands.put("DONE", done); commands.put("INCREMENT", increment); commands.put("NEXT", next); commands.put("SEE", see); CommandController commandController = new MapBasedCommandController(commands); //when {the post method is invoked} HTTPHypermediaRIM rim = new HTTPHypermediaRIM(commandController, new ResourceStateMachine(entrance, new BeanTransformer(), locatorProvider), createMockMetadata(), locatorProvider); Response response = rim.post(mock(HttpHeaders.class), "id", mockUriInfoWithParams(), mockEntityResourceWithId("123")); //then {the response must be HTTP 200 ok} assertEquals(Status.OK.getStatusCode(), response.getStatus()); //and {the response entity must not be null and must be an instance of/subtype RESTResource} assertNotNull(GenericEntity.class.isAssignableFrom(response.getEntity().getClass())); GenericEntity<?> responseEntity = (GenericEntity<?>) response.getEntity(); assertTrue(RESTResource.class.isAssignableFrom(responseEntity.getEntity().getClass())); //and {the response entity must contain a link with a query parameter added by a command appended to it} List<Link> links = new ArrayList<Link>(((RESTResource)responseEntity.getEntity()).getLinks()); assertThat(links.size(), equalTo(1)); //and {the location header must be set to the final resource state resolved //in the sequence of successful autotransitions} assertThat((String)response.getMetadata().get("Location").get(0), allOf(containsString("/baseuri/doorframe"), containsString("mode=walk"))); verify(doSomething, times(1)).execute(any(InteractionContext.class)); verify(increment, times(1)).execute(any(InteractionContext.class)); verify(done, times(0)).execute(any(InteractionContext.class)); } @Test public void testPOSTWithOneConditionalAutomaticTransitionAndDynamicResourceState() throws InteractionException { //given {a graph of resource states comprising of an initial state //and a dynamic state that autotransitions to another resource state} ResourceState entrance = new ResourceState( "house", "entrance", mockSingleAction("DO", Action.TYPE.ENTRY, "POST"), "/entrance", new String[]{"http://temenostech.temenos.com/rels/input"} ); ResourceState doorframe = new ResourceState( "house", "doorframe", mockSingleAction("NEXT", Action.TYPE.VIEW, "GET"), "/doorframe", new String[]{"http://temenostech.temenos.com/rels/next"} ); ResourceState door = new DynamicResourceState("house", "door", "locator", new String[]{"time"}); ResourceState hallway = new ResourceState( "house", "hallway", mockSingleAction("DONE", Action.TYPE.ENTRY, "POST"), "/hallway", (String[])null ); ResourceState curtains = new ResourceState("house", "curtains", mockSingleAction("GET", Action.TYPE.VIEW, "GET"), "/curtains"); ResourceState lighting = new ResourceState("house", "lighting", mockSingleAction("GET", Action.TYPE.VIEW, "GET"), "/lighting"); entrance.addTransition( new Transition.Builder() .flags(Transition.AUTO) .target(doorframe) .evaluation(new ResourceGETExpression(doorframe, ResourceGETExpression.Function.OK)) .build() ); doorframe.addTransition(new Transition.Builder().flags(Transition.AUTO).target(door).build()); hallway.addTransition(new Transition.Builder().method("GET").target(curtains).build()); hallway.addTransition(new Transition.Builder().method("GET").target(lighting).build()); //and {a locator that always returns the hallway resource state when invoked with //string "day"} ResourceLocatorProvider locatorProvider = mock(ResourceLocatorProvider.class); ResourceLocator locator = mock(ResourceLocator.class); when(locator.resolve(eq("day"))).thenReturn(hallway); when(locatorProvider.get(eq("locator"))).thenReturn(locator); Map<String, InteractionCommand> commands = new HashMap<String, InteractionCommand>(); InteractionCommand doSomething = mock(InteractionCommand.class), getSomething = mock(InteractionCommand.class), next = mock(InteractionCommand.class), see = mock(InteractionCommand.class), done = mock(InteractionCommand.class); //and {three InteractionCommands that execute successfully} when(getSomething.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); when(done.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); when(next.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); //and {an InteractionCommand that sets up an alias for resolving a //dynamic resource state and adds query parameters} doAnswer(new Answer<Result>() { @Override public Result answer(InvocationOnMock invocationOnMock) throws Throwable { InteractionContext ctx = ((InteractionContext)invocationOnMock.getArguments()[0]); ctx.setAttribute("time", "day"); ctx.getOutQueryParameters().put("mode", new ArrayList<String>( Arrays.asList(new String[]{"run"})) ); return Result.SUCCESS; } }).when(doSomething).execute(any(InteractionContext.class)); commands.put("GET", getSomething); commands.put("DO", doSomething); commands.put("DONE", done); commands.put("NEXT", next); commands.put("SEE", see); CommandController commandController = new MapBasedCommandController(commands); //when {the post method is invoked} HTTPHypermediaRIM rim = new HTTPHypermediaRIM(commandController, new ResourceStateMachine( entrance, new BeanTransformer(), locatorProvider ), createMockMetadata(), locatorProvider ); Response response = rim.post(mock(HttpHeaders.class), "id", mockUriInfoWithParams(), mockEntityResourceWithId("123")); //then {the response must be HTTP 201 Created} assertEquals(Status.OK.getStatusCode(), response.getStatus()); //and {the response entity must not be null and must be an instance of/subtype RESTResource} assertNotNull(GenericEntity.class.isAssignableFrom(response.getEntity().getClass())); GenericEntity<?> responseEntity = (GenericEntity<?>) response.getEntity(); assertTrue(RESTResource.class.isAssignableFrom(responseEntity.getEntity().getClass())); //and {the response entity must contain a link with a query parameter added by a command appended to it} List<Link> links = new ArrayList<Link>(((RESTResource)responseEntity.getEntity()).getLinks()); assertThat(links.size(), equalTo(3)); assertThat(links.get(1).getHref(), equalTo("/baseuri/curtains?mode=run")); //and {the location header must be set to the final resource resolved //in the sequence of autotransitions} assertThat((String)response.getMetadata().get("Location").get(0), allOf(containsString("/baseuri/hallway"), containsString("mode=run"))); verify(doSomething, times(1)).execute(any(InteractionContext.class)); verify(done, times(1)).execute(any(InteractionContext.class)); } /* * This test is for a POST request that creates a new resource, and uses * linkage parameters to get the resource we transition too */ @Test public void testPOSTwithAutoTransitionLinkageParameters() throws Exception { /* * construct an InteractionContext that simply mocks the result of * creating a resource */ ResourceState initialState = new ResourceState("home", "initial", mockActions(), "/machines"); ResourceState createPsuedoState = new ResourceState(initialState, "create", mockActions()); ResourceState individualMachine = new ResourceState(initialState, "machine", mockActions(), "/{test}"); // create new machine initialState.addTransition(new Transition.Builder().method("POST").target(createPsuedoState).build()); // an auto transition with parameters to the new resource Map<String, String> linkageMap = new HashMap<String, String>(); linkageMap.put("test", "{id}"); createPsuedoState.addTransition(new Transition.Builder().flags(Transition.AUTO).target(individualMachine).uriParameters(linkageMap).build()); // RIM with command controller that issues commands that always return SUCCESS HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState, new BeanTransformer()), createMockMetadata()); Response response = rim.post(mock(HttpHeaders.class), "id", mockEmptyUriInfo(), mockEntityResourceWithId("123")); // null resource @SuppressWarnings("rawtypes") GenericEntity ge = (GenericEntity) response.getEntity(); assertNotNull(ge); RESTResource resource = (RESTResource) ge.getEntity(); assertNotNull(resource); /* * Assert the links in the response match the target resource */ EntityResource<?> createdResource = (EntityResource<?>) ((GenericEntity<?>)response.getEntity()).getEntity(); List<Link> links = new ArrayList<Link>(createdResource.getLinks()); assertEquals(1, links.size()); assertEquals("/baseuri/machines/123", links.get(0).getHref()); } /* * This test is for a GET request that uses a query linkage parameters to get the * resource we transition too. */ @Test public void testGETwithAutoTransitionQueryLinkageParameters() throws Exception { List<Action> actions = new ArrayList<Action>(); actions.add(new Action("GET", Action.TYPE.VIEW)); ResourceState initialState = new ResourceState("home", "initial", actions, "/machines"); // decide whether to go to 'machine' or 'initial' ResourceState conditionPsuedoState = new ResourceState("home", "condition", actions, "/machines/condition"); ResourceState individualMachine = new ResourceState("home", "machine", actions, "/machines/{test}"); // create new machine initialState.addTransition(new Transition.Builder().method("GET").target(conditionPsuedoState).build()); // an auto transition with parameters to the new resource Map<String, String> linkageMap = new HashMap<String, String>(); linkageMap.put("test", "{mytestparam}"); conditionPsuedoState.addTransition(new Transition.Builder().flags(Transition.AUTO).target(individualMachine).uriParameters(linkageMap).build()); // RIM with command controller that issues commands that always return SUCCESS Map<String,InteractionCommand> commands = new HashMap<String,InteractionCommand>(); commands.put("DO", mockCommand_SUCCESS()); // not used commands.put("GET", new InteractionCommand() { @Override public Result execute(InteractionContext ctx) throws InteractionException { ctx.setResource(new EntityResource<String>("")); return Result.SUCCESS; } }); CommandController commandController = new MapBasedCommandController(commands); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(commandController, new ResourceStateMachine(initialState, new BeanTransformer()), createMockMetadata()); Collection<ResourceInteractionModel> children = rim.getChildren(); // find the resource interaction model for the 'exists' state HTTPHypermediaRIM conditionStateRIM = null; for (ResourceInteractionModel r : children) { if (r.getResourcePath().equals("/machines/condition")) { conditionStateRIM = (HTTPHypermediaRIM) r; } } UriInfo uriInfo = mock(UriInfo.class); when(uriInfo.getPathParameters(anyBoolean())).thenReturn(new MultivaluedMapImpl<String>()); MultivaluedMap<String, String> queryParameters = new MultivaluedMapImpl<String>(); queryParameters.add("mytestparam", "123"); when(uriInfo.getQueryParameters(false)).thenReturn(queryParameters); Response response = conditionStateRIM.get(mock(HttpHeaders.class), "id", uriInfo); @SuppressWarnings("rawtypes") GenericEntity ge = (GenericEntity) response.getEntity(); assertNotNull(ge); RESTResource resource = (RESTResource) ge.getEntity(); assertNotNull(resource); /* * Assert the links in the response match the target resource */ EntityResource<?> createdResource = (EntityResource<?>) ((GenericEntity<?>)response.getEntity()).getEntity(); List<Link> links = new ArrayList<Link>(createdResource.getLinks()); assertEquals(1, links.size()); assertEquals("/baseuri/machines/123", links.get(0).getHref()); } private EntityResource<Object> mockEntityResourceWithId(final String id) { return new EntityResource<Object>(new MockEntity(id)); } @SuppressWarnings({ "unchecked" }) @Test public void testBuildResponseWithLinks() throws Exception { // construct an InteractionContext that simply mocks the result of loading a resource ResourceState initialState = new ResourceState("entity", "state", mockActions(), "/path"); InteractionContext testContext = new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), mock(MultivaluedMap.class), mock(MultivaluedMap.class), initialState, mock(Metadata.class)); testContext.setResource(new EntityResource<Object>(null)); // mock 'new InteractionContext()' in call to get whenNew(InteractionContext.class).withParameterTypes(UriInfo.class, HttpHeaders.class, MultivaluedMap.class, MultivaluedMap.class, ResourceState.class, Metadata.class) .withArguments(any(UriInfo.class), any(HttpHeaders.class), any(MultivaluedMap.class), any(MultivaluedMap.class), any(ResourceState.class), any(Metadata.class)).thenReturn(testContext); List<Link> links = new ArrayList<Link>(); links.add(new Link("id", "self", "href", null, null)); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.get(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); RESTResource resourceWithLinks = (RESTResource) ((GenericEntity<?>)response.getEntity()).getEntity(); assertNotNull(resourceWithLinks.getLinks()); assertFalse(resourceWithLinks.getLinks().isEmpty()); assertEquals(1, resourceWithLinks.getLinks().size()); Link link = (Link) resourceWithLinks.getLinks().toArray()[0]; assertEquals("self", link.getRel()); } @SuppressWarnings({ "unchecked" }) @Test public void testBuildResponseWithDynamicLinkParams() throws Exception { // construct an InteractionContext that simply mocks the result of loading a resource ResourceState initialState = new ResourceState("entity", "state", mockActions(), "/path"); ResourceState b = new ResourceState(initialState, "b", new ArrayList<Action>()); Transition t = new Transition.Builder().source(initialState).method("GET").target(b).build(); initialState.addTransition(t); InteractionContext testContext = new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), mock(MultivaluedMap.class), mock(MultivaluedMap.class), initialState, mock(Metadata.class)); MultivaluedMap<String, String> outQueryParameters = testContext.getOutQueryParameters(); outQueryParameters.add("penguin", "emperor"); testContext.setResource(new EntityResource<Object>(null)); // mock 'new InteractionContext()' in call to get whenNew(InteractionContext.class).withParameterTypes(UriInfo.class, HttpHeaders.class, MultivaluedMap.class, MultivaluedMap.class, ResourceState.class, Metadata.class) .withArguments(any(UriInfo.class), any(HttpHeaders.class), any(MultivaluedMap.class), any(MultivaluedMap.class), any(ResourceState.class), any(Metadata.class)).thenReturn(testContext); List<Link> links = new ArrayList<Link>(); links.add(new Link("id", "self", "href", null, null)); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.get(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); RESTResource resourceWithLinks = (RESTResource) ((GenericEntity<?>)response.getEntity()).getEntity(); assertNotNull(resourceWithLinks.getLinks()); assertFalse(resourceWithLinks.getLinks().isEmpty()); assertEquals(2, resourceWithLinks.getLinks().size()); Link link = (Link) resourceWithLinks.getLinks().toArray()[0]; assertEquals("self", link.getRel()); link = (Link)resourceWithLinks.getLinks().toArray()[1]; assertEquals("item", link.getRel()); assertEquals("/baseuri/path?penguin=emperor", link.getHref()); } @SuppressWarnings({ "unchecked" }) @Test public void testBuildResponseWithMultipleDynamicLinkParams() throws Exception { // construct an InteractionContext that simply mocks the result of loading a resource ResourceState initialState = new ResourceState("entity", "state", mockActions(), "/path"); ResourceState b = new ResourceState(initialState, "b", new ArrayList<Action>()); Transition t = new Transition.Builder().source(initialState).method("GET").target(b).build(); initialState.addTransition(t); InteractionContext testContext = new InteractionContext(mock(UriInfo.class), mock(HttpHeaders.class), mock(MultivaluedMap.class), mock(MultivaluedMap.class), initialState, mock(Metadata.class)); MultivaluedMap<String, String> outQueryParameters = testContext.getOutQueryParameters(); outQueryParameters.add("penguin", "emperor"); outQueryParameters.add("crispbread", "coconut"); outQueryParameters.add("penguin", "black"); testContext.setResource(new EntityResource<Object>(null)); // mock 'new InteractionContext()' in call to get whenNew(InteractionContext.class).withParameterTypes(UriInfo.class, HttpHeaders.class, MultivaluedMap.class, MultivaluedMap.class, ResourceState.class, Metadata.class) .withArguments(any(UriInfo.class), any(HttpHeaders.class), any(MultivaluedMap.class), any(MultivaluedMap.class), any(ResourceState.class), any(Metadata.class)).thenReturn(testContext); List<Link> links = new ArrayList<Link>(); links.add(new Link("id", "self", "href", null, null)); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.get(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); RESTResource resourceWithLinks = (RESTResource) ((GenericEntity<?>)response.getEntity()).getEntity(); assertNotNull(resourceWithLinks.getLinks()); assertFalse(resourceWithLinks.getLinks().isEmpty()); assertEquals(2, resourceWithLinks.getLinks().size()); Link link = (Link) resourceWithLinks.getLinks().toArray()[0]; assertEquals("self", link.getRel()); link = (Link)resourceWithLinks.getLinks().toArray()[1]; assertEquals("item", link.getRel()); assertEquals("/baseuri/path?penguin=emperor&penguin=black&crispbread=coconut", link.getHref()); } @SuppressWarnings({ "unchecked" }) @Test public void testBuildResponseEntityName() throws Exception { // construct an InteractionContext that simply mocks the result of loading a resource ResourceState initialState = new ResourceState("entity", "state", mockActions(), "/path"); InteractionContext testContext = new InteractionContext(mock(UriInfo.class), null, mock(MultivaluedMap.class), mock(MultivaluedMap.class), initialState, mock(Metadata.class)); testContext.setResource(new EntityResource<Object>(null)); // mock 'new InteractionContext()' in call to get whenNew(InteractionContext.class).withParameterTypes(UriInfo.class, HttpHeaders.class, MultivaluedMap.class, MultivaluedMap.class, ResourceState.class, Metadata.class) .withArguments(any(UriInfo.class), any(HttpHeaders.class), any(MultivaluedMap.class), any(MultivaluedMap.class), any(ResourceState.class), any(Metadata.class)).thenReturn(testContext); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockNoopCommandController(), new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.get(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); RESTResource resource = (RESTResource) ((GenericEntity<?>)response.getEntity()).getEntity(); assertNotNull(resource.getEntityName()); assertEquals("entity", resource.getEntityName()); } @SuppressWarnings({ "unchecked" }) private UriInfo mockEmptyUriInfo() { UriInfo uriInfo = mock(UriInfo.class); when(uriInfo.getPathParameters(anyBoolean())).thenReturn(mock(MultivaluedMap.class)); when(uriInfo.getQueryParameters(false)).thenReturn(mock(MultivaluedMap.class)); return uriInfo; } private UriInfo mockUriInfoWithParams() { UriInfo uriInfo = mock(UriInfo.class); MultivaluedMap<String, String> queryParam = new MultivaluedMapImpl<String>(); queryParam.add("mode", "walk"); when(uriInfo.getPathParameters(anyBoolean())).thenReturn(new MultivaluedMapImpl<String>()); when(uriInfo.getQueryParameters(false)).thenReturn(queryParam); when(uriInfo.getQueryParameters()).thenReturn(queryParam); return uriInfo; } private CommandController mockNoopCommandController() { // make sure command execution does nothing CommandController commandController = mock(CommandController.class); InteractionCommand testCommand = mockCommand_SUCCESS(); when(commandController.isValidCommand(anyString())).thenReturn(true); when(commandController.fetchCommand(anyString())).thenReturn(testCommand); return commandController; } // create command returning the supplied entity private InteractionCommand createCommand(final String entityName, final Entity entity, final InteractionCommand.Result result) { InteractionCommand command = new InteractionCommand() { public Result execute(InteractionContext ctx) { if (entity != null) { ctx.setResource(new EntityResource<Entity>(entityName, entity)); } return result; } }; return command; } private TransitionCommand createTransitionCommand(final String entityName, final Entity entity, final InteractionCommand.Result result) { TransitionCommand command = new TransitionCommand() { public Result execute(InteractionContext ctx) { if (entity != null) { ctx.setResource(new EntityResource<Entity>(entityName, entity)); } return result; } public boolean isInterim() { return false; } }; return command; } private InteractionCommand mockCommand_SUCCESS() { InteractionCommand mockCommand = mock(InteractionCommand.class); try { when(mockCommand.execute(any(InteractionContext.class))).thenReturn(Result.SUCCESS); } catch(InteractionException ie) { Assert.fail(ie.getMessage()); } return mockCommand; } private InteractionCommand mockCommand_FAILURE() { InteractionCommand mockCommand = mock(InteractionCommand.class); try { when(mockCommand.execute(any(InteractionContext.class))).thenReturn(Result.FAILURE); } catch(InteractionException ie) { Assert.fail(ie.getMessage()); } return mockCommand; } private TransitionCommand mockTransitionCommand_FAILURE() { TransitionCommand mockCommand = mock(TransitionCommand.class); try { when(mockCommand.execute(any(InteractionContext.class))).thenReturn(Result.FAILURE); } catch(InteractionException ie) { Assert.fail(ie.getMessage()); } return mockCommand; } /* * This test is for an OPTIONS request. * A OPTIONS request uses a GET command, the response must include an Allow header * and no body plus HttpStatus 204 "No Content". */ @Test public void testOPTIONSBuildResponseWithNoContent() throws Exception { ResourceState initialState = new ResourceState("entity", "state", mockActions(), "/path"); initialState.addTransition(new Transition.Builder().method(HttpMethod.GET).target(initialState).build()); /* * Construct an InteractionCommand that simply mocks the result of * a successful command. */ final InteractionCommand mockCommand = new InteractionCommand() { @Override public Result execute(InteractionContext ctx) { ctx.setResource(new EntityResource<Object>(null)); return Result.SUCCESS; } }; // create mock command controller MapBasedCommandController mockCommandController = new MapBasedCommandController(); mockCommandController.setCommandMap(new HashMap<String, InteractionCommand>(){ private static final long serialVersionUID = 1L; { put("GET", mockCommand); put("DO", mockCommand); } }); // RIM with command controller that issues our mock InteractionCommand HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockCommandController, new ResourceStateMachine(initialState), createMockMetadata()); Response response = rim.options(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); // 204 http status for no content assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus()); // check Allow header Object allow = response.getMetadata().getFirst("Allow"); assertNotNull(allow); String[] allows = allow.toString().split(", "); assertEquals(3, allows.length); List<String> allowsList = Arrays.asList(allows); assertTrue(allowsList.contains("GET")); assertTrue(allowsList.contains("OPTIONS")); assertTrue(allowsList.contains("HEAD")); } /* * This test checks that a 503 error returns a correct response */ @SuppressWarnings("unchecked") @Test public void testBuildResponseWith503ServiceUnavailable() { Response response = getMockResponse( getInteractionExceptionMockCommand(Status.SERVICE_UNAVAILABLE, "Failed to connect to resource manager."), new GETExceptionCommand() ); assertEquals(Response.Status.SERVICE_UNAVAILABLE.getStatusCode(), response.getStatus()); GenericEntity<?> ge = (GenericEntity<?>) response.getEntity(); assertNotNull("Excepted a response body", ge); if(ResourceTypeHelper.isType(ge.getRawType(), ge.getType(), EntityResource.class, GenericError.class)) { EntityResource<GenericError> er = (EntityResource<GenericError>) ge.getEntity(); GenericError error = er.getEntity(); assertEquals("503", error.getCode()); assertEquals("Failed to connect to resource manager.", error.getMessage()); } else { fail("Response body is not a generic error entity resource type."); } } /* * This test checks returning a 503 error without a response body */ @Test public void testBuildResponseWith503ServiceUnavailableWithoutResponseBody() { Response response = getMockResponse(getInteractionExceptionMockCommand(Status.SERVICE_UNAVAILABLE, null)); assertEquals(Response.Status.SERVICE_UNAVAILABLE.getStatusCode(), response.getStatus()); assertNull(response.getEntity()); } /* * This test checks that a 504 error returns a correct response */ @SuppressWarnings("unchecked") @Test public void testBuildResponseWith504GatewayTimeout() { Response response = getMockResponse( getInteractionExceptionMockCommand(HttpStatusTypes.GATEWAY_TIMEOUT, "Request timeout."), new GETExceptionCommand() ); assertEquals(HttpStatusTypes.GATEWAY_TIMEOUT.getStatusCode(), response.getStatus()); GenericEntity<?> ge = (GenericEntity<?>) response.getEntity(); assertNotNull("Excepted a response body", ge); if(ResourceTypeHelper.isType(ge.getRawType(), ge.getType(), EntityResource.class, GenericError.class)) { EntityResource<GenericError> er = (EntityResource<GenericError>) ge.getEntity(); GenericError error = er.getEntity(); assertEquals("504", error.getCode()); assertEquals("Request timeout.", error.getMessage()); } else { fail("Response body is not a generic error entity resource type."); } } /* * This test checks that a 500 error returns a proper error message inside * the body of the response. */ @SuppressWarnings("unchecked") @Test public void testBuildResponseWith500InternalServerError() { ResourceState initialState = new ResourceState("home", "initial", mockActions(), "/machines"); ResourceState createPsuedoState = new ResourceState(initialState, "create", mockActions()); // create new machine initialState.addTransition(new Transition.Builder().method("POST").target(createPsuedoState).build()); Map<String,InteractionCommand> commands = new HashMap<String,InteractionCommand>(); commands.put("DO", getGenericErrorMockCommand(Result.FAILURE, "Resource manager: 5 fatal error and 2 warnings.")); commands.put("GET", mockCommand_SUCCESS()); CommandController commandController = new MapBasedCommandController(commands); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(commandController, new ResourceStateMachine(initialState, new BeanTransformer()), createMockMetadata()); Response response = rim.post(mock(HttpHeaders.class), "id", mockEmptyUriInfo(), mockEntityResourceWithId("123")); assertEquals(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); GenericEntity<?> ge = (GenericEntity<?>) response.getEntity(); assertNotNull("Excepted a response body", ge); if(ResourceTypeHelper.isType(ge.getRawType(), ge.getType(), EntityResource.class, GenericError.class)) { EntityResource<GenericError> er = (EntityResource<GenericError>) ge.getEntity(); GenericError error = er.getEntity(); assertEquals("FAILURE", error.getCode()); assertEquals("Resource manager: 5 fatal error and 2 warnings.", error.getMessage()); } else { fail("Response body is not a generic error entity resource type."); } } /* * This test checks that a 400 error returns a proper error message inside * the body of the response. */ @SuppressWarnings("unchecked") @Test public void testBuildResponseWith400BadRequest() { Response response = getMockResponse(getGenericErrorMockCommand(Result.INVALID_REQUEST, "Resource manager: 4 validation errors.")); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); GenericEntity<?> ge = (GenericEntity<?>) response.getEntity(); assertNotNull("Excepted a response body", ge); if(ResourceTypeHelper.isType(ge.getRawType(), ge.getType(), EntityResource.class, GenericError.class)) { EntityResource<GenericError> er = (EntityResource<GenericError>) ge.getEntity(); GenericError error = er.getEntity(); assertEquals("INVALID_REQUEST", error.getCode()); assertEquals("Resource manager: 4 validation errors.", error.getMessage()); } else { fail("Response body is not a generic error entity resource type."); } } /* * This test checks that a 403 error returns a proper status code */ @SuppressWarnings("unchecked") @Test public void testBuildResponseWith403AuthorisationFailure() { Response response = getMockResponse( getInteractionExceptionMockCommand(Status.FORBIDDEN, "User is not allowed to access this resource."), new GETExceptionCommand() ); assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus()); GenericEntity<?> ge = (GenericEntity<?>) response.getEntity(); assertNotNull("Excepted a response body", ge); if(ResourceTypeHelper.isType(ge.getRawType(), ge.getType(), EntityResource.class, GenericError.class)) { EntityResource<GenericError> er = (EntityResource<GenericError>) ge.getEntity(); GenericError error = er.getEntity(); assertEquals("403", error.getCode()); assertEquals("User is not allowed to access this resource.", error.getMessage()); } else { fail("Response body is not a generic error entity resource type."); } } /* * This test checks that a 500 error is returned when a * command throws an exception. */ @Test public void testGETCommandThrowsException() { try { getMockResponse(getRuntimeExceptionMockCommand("Unknown fatal error.")); fail("Test failed to throw a runtime exception"); } catch(RuntimeException re) { assertEquals("Unknown fatal error.", re.getMessage()); } } /* * Test to ensure command can cause 404 error if a specific entity is not available. */ @Test public void testGETBuildResponseWith404NotFound() { Response response = getMockResponse(mockCommand_FAILURE()); assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); } /* * Test to ensure command can cause 404 error without a response body if a specific entity is not available. */ @Test public void testGETBuildResponseWith404NotFoundWithoutResponseBody() { Response response = getMockResponse(mockCommand_FAILURE()); assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); assertNull(response.getEntity()); } /* * Test to ensure command can cause 404 error if a specific entity is not available. */ @SuppressWarnings("unchecked") @Test public void testGETBuildResponseWith404NotFoundInteractionException() { Response response = getMockResponse( getInteractionExceptionMockCommand(Status.NOT_FOUND, "Resource manager: entity Fred not found or currently unavailable."), new GETExceptionCommand() ); assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus()); GenericEntity<?> ge = (GenericEntity<?>) response.getEntity(); assertNotNull("Excepted a response body", ge); if(ResourceTypeHelper.isType(ge.getRawType(), ge.getType(), EntityResource.class, GenericError.class)) { EntityResource<GenericError> er = (EntityResource<GenericError>) ge.getEntity(); GenericError error = er.getEntity(); assertEquals("404", error.getCode()); assertEquals("Resource manager: entity Fred not found or currently unavailable.", error.getMessage()); } else { fail("Response body is not a generic error entity resource type."); } } /* * Test to ensure command can cause 404 error if a specific entity is not available on DELETE. */ @Test public void testDELETEBuildResponseWith404NotFound() { List<Action> actions = new ArrayList<Action>(); actions.add(new Action("DO", Action.TYPE.ENTRY)); /* * construct an InteractionContext that simply mocks the result of * creating a resource */ ResourceState initialState = new ResourceState("home", "initial", mockActions(), "/machines"); ResourceState individualMachine = new ResourceState(initialState, "machine", actions, "/{id}"); // create new machine initialState.addTransition(new Transition.Builder().method("DELETE").target(individualMachine).build()); // RIM with command controller that issues commands that return SUCCESS for 'DO' action and FAILURE for 'GET' action (see mockActions()) Map<String,InteractionCommand> commands = new HashMap<String,InteractionCommand>(); commands.put("DO", mockCommand_FAILURE()); commands.put("GET", mockCommand_SUCCESS()); CommandController commandController = new MapBasedCommandController(commands); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(commandController, new ResourceStateMachine(initialState, new BeanTransformer()), createMockMetadata()); HTTPHypermediaRIM deleteInteraction = (HTTPHypermediaRIM) rim.getChildren().iterator().next(); Response response = deleteInteraction.delete(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus()); } /* * This test checks that a 400 error returns a proper error message inside * the body of the response. */ @SuppressWarnings("unchecked") @Test public void testBuildResponseWith400BadRequestFromErrorResource() { Response response = getMockResponseWithErrorResource(getGenericErrorMockCommand(Result.INVALID_REQUEST, "Resource manager: 4 validation errors.")); assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus()); GenericEntity<?> ge = (GenericEntity<?>) response.getEntity(); assertNotNull("Excepted a response body", ge); if(ResourceTypeHelper.isType(ge.getRawType(), ge.getType(), EntityResource.class, GenericError.class)) { EntityResource<GenericError> er = (EntityResource<GenericError>) ge.getEntity(); assertEquals("ErrorEntity", er.getEntityName()); GenericError error = er.getEntity(); assertEquals("INVALID_REQUEST", error.getCode()); assertEquals("Resource manager: 4 validation errors.", error.getMessage()); assertNotNull(er.getLinks()); assertFalse(er.getLinks().isEmpty()); assertEquals(1, er.getLinks().size()); Link link = (Link) er.getLinks().toArray()[0]; assertEquals("self", link.getRel()); } else { fail("Response body is not a generic error entity resource type."); } } @Test public void testGETWithETagHeader() throws InteractionException { //Create mock command InteractionCommand mockCommand = new InteractionCommand() { @Override public Result execute(InteractionContext ctx) throws InteractionException { RESTResource resource = new EntityResource<Object>(null); resource.setEntityTag("ABCDEFG"); ctx.setResource(resource); return Result.SUCCESS; } }; //Process mock command and check the response Response response = getMockResponse(mockCommand); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); List<Object> etagHeader = response.getMetadata().get(HttpHeaders.ETAG); assertNotNull(etagHeader); assertEquals(1, etagHeader.size()); assertEquals("ABCDEFG", etagHeader.get(0)); } @Test public void testGETWithoutETagHeader() throws InteractionException { //Create mock command InteractionCommand mockCommand = new InteractionCommand() { @Override public Result execute(InteractionContext ctx) throws InteractionException { ctx.setResource(new EntityResource<Object>(null)); return Result.SUCCESS; } }; //Process mock command and check the response Response response = getMockResponse(mockCommand); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); List<Object> etagHeader = response.getMetadata().get(HttpHeaders.ETAG); assertNull(etagHeader); } @Test public void testGETWithEmptyETagHeader() throws InteractionException { //Create mock command InteractionCommand mockCommand = new InteractionCommand() { @Override public Result execute(InteractionContext ctx) throws InteractionException { RESTResource resource = new EntityResource<Object>(null); resource.setEntityTag(""); ctx.setResource(resource); return Result.SUCCESS; } }; //Process mock command and check the response Response response = getMockResponse(mockCommand); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); List<Object> etagHeader = response.getMetadata().get(HttpHeaders.ETAG); assertNull(etagHeader); } /* * This test checks that a 412 error returns a proper error message inside * the body of the response. */ @SuppressWarnings("unchecked") @Test public void testBuildResponseWith412PreconditionFailed() { Response response = getMockResponse(getGenericErrorMockCommand(Result.CONFLICT, "Resource has been modified by somebody else.")); assertEquals(Response.Status.PRECONDITION_FAILED.getStatusCode(), response.getStatus()); GenericEntity<?> ge = (GenericEntity<?>) response.getEntity(); assertNotNull("Excepted a response body", ge); if(ResourceTypeHelper.isType(ge.getRawType(), ge.getType(), EntityResource.class, GenericError.class)) { EntityResource<GenericError> er = (EntityResource<GenericError>) ge.getEntity(); GenericError error = er.getEntity(); assertEquals("CONFLICT", error.getCode()); assertEquals("Resource has been modified by somebody else.", error.getMessage()); } else { fail("Response body is not a generic error entity resource type."); } } /* * Test to ensure we return a 304 Not modified if the etag of the response is the same * as the etag on the request's If-None-Match header. */ @Test public void testBuildResponseWith304NotModified() { HttpHeaders httpHeaders = mock(HttpHeaders.class); doAnswer(new Answer<List<String>>() { @SuppressWarnings("serial") @Override public List<String> answer(InvocationOnMock invocation) throws Throwable { String headerName = (String) invocation.getArguments()[0]; if(headerName.equals(HttpHeaders.IF_NONE_MATCH)) { return new ArrayList<String>() {{ add("ABCDEFG"); }}; } return null; } }).when(httpHeaders).getRequestHeader(any(String.class)); Response response = getMockResponse(getEntityMockCommand("TestEntity", new EntityProperties(), "ABCDEFG"), null, httpHeaders); assertEquals(Response.Status.NOT_MODIFIED.getStatusCode(), response.getStatus()); GenericEntity<?> ge = (GenericEntity<?>) response.getEntity(); assertNull("Should not have a response body", ge); } /* * Test to ensure we return a 200 Success if the etag of the response is not the same * as the etag on the request's If-None-Match header. */ @SuppressWarnings("unchecked") @Test public void testBuildResponseGETModifiedResource() { HttpHeaders httpHeaders = mock(HttpHeaders.class); doAnswer(new Answer<List<String>>() { @SuppressWarnings("serial") @Override public List<String> answer(InvocationOnMock invocation) throws Throwable { String headerName = (String) invocation.getArguments()[0]; if(headerName.equals(HttpHeaders.IF_NONE_MATCH)) { return new ArrayList<String>() {{ add("ABCDEFG"); }}; } return null; } }).when(httpHeaders).getRequestHeader(any(String.class)); Response response = getMockResponse(getEntityMockCommand("TestEntity", new EntityProperties(), "IJKLMNO"), null, httpHeaders); assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); GenericEntity<?> ge = (GenericEntity<?>) response.getEntity(); assertNotNull("Expected a response body", ge); if(ResourceTypeHelper.isType(ge.getRawType(), ge.getType(), EntityResource.class, Entity.class)) { EntityResource<Entity> er = (EntityResource<Entity>) ge.getEntity(); assertEquals("TestEntity", er.getEntity().getName()); assertEquals("IJKLMNO", er.getEntityTag()); //Response should have new etag } else { fail("Response body is not an entity resource type."); } } /* * This test is for a POST request that creates a new resource, and returns * the links for the resource we auto transition to. */ @Test public void testPOSTwithAutoTransitions() throws Exception { /* * construct an InteractionContext that simply mocks the result of * creating a resource */ ResourceState initialState = new ResourceState("home", "initial", mockActions(new Action("POST", Action.TYPE.ENTRY, new Properties(), "POST")), "/machines"); ResourceState createPsuedoState = new ResourceState(initialState, "create", mockActions()); ResourceState individualMachine = new ResourceState(initialState, "machine", mockActions(new Action("DO", Action.TYPE.ENTRY)), "/individualMachine1/{id}"); individualMachine.addTransition(new Transition.Builder().method("GET").target(initialState).build()); ResourceState individualMachine2 = new ResourceState(initialState, "machine2", mockActions(new Action("GET", Action.TYPE.VIEW)), "/individualMachine2/{id}"); individualMachine.addTransition(new Transition.Builder().method("GET").target(initialState).build()); // create new machine initialState.addTransition(new Transition.Builder().method("POST").target(createPsuedoState).build()); // an auto transition to the new resource Map<String, String> uriLinkageMap = new HashMap<String, String>(); uriLinkageMap.put("id", "{id}"); //Add the first auto-transition createPsuedoState.addTransition(new Transition.Builder().flags(Transition.AUTO).target(individualMachine).uriParameters(uriLinkageMap).build()); //Add a second auto-transition createPsuedoState.addTransition(new Transition.Builder().flags(Transition.AUTO).target(individualMachine2).uriParameters(uriLinkageMap).build()); MapBasedCommandController commandController = new MapBasedCommandController(); commandController.getCommandMap().put("DO", createTransitionCommand("entity", null, Result.FAILURE)); commandController.getCommandMap().put("GET", createTransitionCommand("entity", null, Result.SUCCESS)); commandController.getCommandMap().put("POST", createCommand("entity", null, Result.CREATED)); // RIM with command controller that issues commands that always return SUCCESS HTTPHypermediaRIM rim = new HTTPHypermediaRIM(commandController, new ResourceStateMachine(initialState, new BeanTransformer()), createMockMetadata()); Response response = rim.post(mock(HttpHeaders.class), "id", mockEmptyUriInfo(), mockEntityResourceWithId("123")); // null resource @SuppressWarnings("rawtypes") GenericEntity ge = (GenericEntity) response.getEntity(); assertNotNull(ge); RESTResource resource = (RESTResource) ge.getEntity(); assertNotNull(resource); /* * Assert the links in the response match the target resource */ EntityResource<?> createdResource = (EntityResource<?>) ((GenericEntity<?>)response.getEntity()).getEntity(); List<Link> links = new ArrayList<Link>(createdResource.getLinks()); assertEquals(1, links.size()); assertEquals("machine2", links.get(0).getTitle()); assertEquals("/baseuri/machines/individualMachine2/123", links.get(0).getHref()); } protected Response getMockResponse(InteractionCommand mockCommand) { return this.getMockResponse(mockCommand, null); } protected Response getMockResponse(InteractionCommand mockCommand, InteractionCommand mockExceptionCommand) { return this.getMockResponse(mockCommand, mockExceptionCommand, mock(HttpHeaders.class)); } protected Response getMockResponse(final InteractionCommand mockCommand, InteractionCommand mockExceptionCommand, HttpHeaders httpHeaders) { MapBasedCommandController mockCommandController = mock(MapBasedCommandController.class); mockCommandController.setCommandMap(new HashMap<String, InteractionCommand>(){ private static final long serialVersionUID = 1L; { put("GET", mockCommand); } }); when(mockCommandController.fetchCommand("GET")).thenReturn(mockCommand); when(mockCommandController.fetchCommand("DO")).thenReturn(mockCommand); if(mockExceptionCommand != null) { mockCommandController.getCommandMap().put("GETException", mockExceptionCommand); when(mockCommandController.fetchCommand("GETException")).thenReturn(mockExceptionCommand); } ResourceState initialState = new ResourceState("entity", "state", mockActions(), "/path"); initialState.setInitial(true); ResourceState exceptionState = null; if(mockExceptionCommand != null) { exceptionState = new ResourceState("exception", "exceptionState", mockExceptionActions(), "/exception"); exceptionState.setException(true); } HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockCommandController, new ResourceStateMachine(initialState, exceptionState), createMockMetadata()); return rim.get(httpHeaders, "id", mockEmptyUriInfo()); } protected Response getMockResponseWithErrorResource(final InteractionCommand mockCommand) { MapBasedCommandController mockCommandController = mock(MapBasedCommandController.class); final InteractionCommand noopCommand = new NoopGETCommand(); mockCommandController.setCommandMap(new HashMap<String, InteractionCommand>(){ private static final long serialVersionUID = 1L; { put("GET", mockCommand); put("noop", noopCommand); } }); when(mockCommandController.fetchCommand("GET")).thenReturn(mockCommand); when(mockCommandController.fetchCommand("DO")).thenReturn(mockCommand); when(mockCommandController.fetchCommand("noop")).thenReturn(noopCommand); ResourceState errorState = new ResourceState("ErrorEntity", "errorState", mockErrorActions(), "/error"); ResourceState initialState = new ResourceState("entity", "state", mockActions(), "/path", null, null, errorState); initialState.setInitial(true); HTTPHypermediaRIM rim = new HTTPHypermediaRIM(mockCommandController, new ResourceStateMachine(initialState), createMockMetadata()); return rim.get(mock(HttpHeaders.class), "id", mockEmptyUriInfo()); } protected InteractionCommand getGenericErrorMockCommand(final InteractionCommand.Result result, final String body) { InteractionCommand mockCommand = new InteractionCommand() { @Override public Result execute(InteractionContext ctx) { if(body != null) { ctx.setResource(createGenericErrorResource(new GenericError(result.toString(), body))); } return result; } }; return mockCommand; } protected InteractionCommand getInteractionExceptionMockCommand(final StatusType status, final String message) { InteractionCommand mockCommand = new InteractionCommand() { @Override public Result execute(InteractionContext ctx) throws InteractionException { throw new InteractionException(status, message); } }; return mockCommand; } protected InteractionCommand getRuntimeExceptionMockCommand(final String errorMessage) { InteractionCommand mockCommand = new InteractionCommand() { @Override public Result execute(InteractionContext ctx) { throw new RuntimeException(errorMessage); } }; return mockCommand; } protected InteractionCommand getEntityMockCommand(final String entityName, final EntityProperties entityProperties, final String etag) { InteractionCommand mockCommand = new InteractionCommand() { @Override public Result execute(InteractionContext ctx) { RESTResource resource = CommandHelper.createEntityResource(new Entity(entityName, entityProperties)); resource.setEntityTag(etag); ctx.setResource(resource); return Result.SUCCESS; } }; return mockCommand; } public static EntityResource<GenericError> createGenericErrorResource(GenericError error){ return CommandHelper.createEntityResource(error, GenericError.class); } }