package com.temenos.interaction.core.hypermedia.validation; /* * #%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.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import org.junit.Test; import com.temenos.interaction.core.entity.EntityMetadata; import com.temenos.interaction.core.entity.Metadata; import com.temenos.interaction.core.hypermedia.Action; import com.temenos.interaction.core.hypermedia.CollectionResourceState; 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.hypermedia.expression.Expression; import com.temenos.interaction.core.hypermedia.expression.ResourceGETExpression; import com.temenos.interaction.core.hypermedia.expression.SimpleLogicalExpressionEvaluator; public class TestHypermediaValidator { @Test public void testValidateStatesValid() { String ENTITY_NAME = ""; ResourceState begin = new ResourceState(ENTITY_NAME, "begin", new ArrayList<Action>(), "{id}"); ResourceState exists = new ResourceState(ENTITY_NAME, "exists", new ArrayList<Action>(), "{id}"); ResourceState end = new ResourceState(ENTITY_NAME, "end", new ArrayList<Action>(), "{id}"); begin.addTransition(new Transition.Builder().method("PUT").target(exists).build()); exists.addTransition(new Transition.Builder().method("DELETE").target(end).build()); Set<ResourceState> states = new HashSet<ResourceState>(); states.add(begin); states.add(exists); states.add(end); ResourceStateMachine sm = new ResourceStateMachine(begin); Metadata metadata = new Metadata(""); metadata.setEntityMetadata(new EntityMetadata("")); HypermediaValidator v = HypermediaValidator.createValidator(sm, metadata); assertTrue(v.validate(states, sm)); } @Test public void testValidateStatesUnreachable() { String ENTITY_NAME = ""; ResourceState begin = new ResourceState(ENTITY_NAME, "begin", new ArrayList<Action>(), "{id}"); ResourceState exists = new ResourceState(ENTITY_NAME, "exists", new ArrayList<Action>(), "{id}"); ResourceState end = new ResourceState(ENTITY_NAME, "end", new ArrayList<Action>(), "{id}"); begin.addTransition(new Transition.Builder().method("PUT").target(exists).build()); exists.addTransition(new Transition.Builder().method("DELETE").target(end).build()); ResourceState unreachableState = new ResourceState(ENTITY_NAME, "unreachable", new ArrayList<Action>(), "/unreachable"); Set<ResourceState> states = new HashSet<ResourceState>(); states.add(begin); states.add(exists); states.add(unreachableState); states.add(end); ResourceStateMachine sm = new ResourceStateMachine(begin); Metadata metadata = new Metadata(""); metadata.setEntityMetadata(new EntityMetadata("")); HypermediaValidator v = HypermediaValidator.createValidator(sm, metadata); assertFalse(v.validate(states, sm)); } @Test public void testValidateMetadataNotFound() { String ENTITY_NAME = "root_entity"; ResourceState root = new ResourceState(ENTITY_NAME, "root", new ArrayList<Action>(), "/root"); Metadata metadata = new Metadata(""); metadata.setEntityMetadata(new EntityMetadata("root_entity")); ResourceStateMachine sm = new ResourceStateMachine(root); HypermediaValidator v = HypermediaValidator.createValidator(sm, metadata); assertFalse(v.validate()); } @Test public void testDOTExceptionResource() { String expected = "digraph G {\n" + " Ginitial[shape=circle, width=.25, label=\"\", color=black, style=filled]\n" + " EXCEPTIONexception[label=\"EXCEPTION.exception\"]\n" + " Gexists[label=\"G.exists /entities/{id}\"]\n" + " Ginitial->Gexists[label=\"PUT /entities/{id}\"]\n" + " final[shape=circle, width=.25, label=\"\", color=black, style=filled, peripheries=2]\n" + " Gexists->final[label=\"\"]\n}"; String ENTITY_NAME = "G"; ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/"); ResourceState exists = new ResourceState(ENTITY_NAME, "exists", new ArrayList<Action>(), "/entities/{id}"); ResourceState exception = new ResourceState("EXCEPTION", "exception", new ArrayList<Action>(), "/"); exception.setException(true); initial.addTransition(new Transition.Builder().method("PUT").target(exists).build()); ResourceStateMachine sm = new ResourceStateMachine(initial, exception, null, null); Metadata metadata = new Metadata(""); metadata.setEntityMetadata(new EntityMetadata("")); HypermediaValidator v = HypermediaValidator.createValidator(sm, metadata); String result = v.graph(); System.out.println("DOTException: \n" + result); assertEquals(expected, result); } @Test public void testDOTTransitionEval() { String expected = "digraph G {\n" + " Ginitial[shape=circle, width=.25, label=\"\", color=black, style=filled]\n" + " Gother[label=\"G.other /entities/{id}\"]\n" + " final[shape=circle, width=.25, label=\"\", color=black, style=filled, peripheries=2]\n" + " Gother->final[label=\"\"]\n" + " Ginitial->Gother[label=\"PUT (OK(other)) /entities/{id}\"]\n" + "}"; String ENTITY_NAME = "G"; ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/"); ResourceState other = new ResourceState(ENTITY_NAME, "other", new ArrayList<Action>(), "/entities/{id}"); List<Expression> expressions = new ArrayList<Expression>(); expressions.add(new ResourceGETExpression(other, ResourceGETExpression.Function.OK)); initial.addTransition(new Transition.Builder().method("PUT").target(other).evaluation(new SimpleLogicalExpressionEvaluator(expressions)).build()); ResourceStateMachine sm = new ResourceStateMachine(initial); Metadata metadata = new Metadata(""); metadata.setEntityMetadata(new EntityMetadata("G")); HypermediaValidator v = HypermediaValidator.createValidator(sm, metadata); String result = v.graph(); System.out.println("DOTTransitionEval: \n" + result); assertEquals(expected, result); } @Test public void testDOT204NoContent() { String expected = "digraph G {\n" + " Ginitial[shape=circle, width=.25, label=\"\", color=black, style=filled]\n" + " Gexists[label=\"G.exists /entities/{id}\"]\n" + " Gdeleted[label=\"G.deleted /entities/{id}\"]\n" + " Ginitial->Gexists[label=\"PUT /entities/{id}\"]\n" + " Gexists->Gdeleted[label=\"DELETE /entities/{id}\"]\n" + " final[shape=circle, width=.25, label=\"\", color=black, style=filled, peripheries=2]\n Gdeleted->final[label=\"\"]\n}"; String ENTITY_NAME = "G"; ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/"); ResourceState exists = new ResourceState(ENTITY_NAME, "exists", new ArrayList<Action>(), "/entities/{id}"); ResourceState deleted = new ResourceState(ENTITY_NAME, "deleted", new ArrayList<Action>(), "/entities/{id}"); initial.addTransition(new Transition.Builder().method("PUT").target(exists).build()); // a transition to a final state will result in 204 (No Content) at runtime exists.addTransition(new Transition.Builder().method("DELETE").target(deleted).build()); ResourceStateMachine sm = new ResourceStateMachine(initial); Metadata metadata = new Metadata(""); metadata.setEntityMetadata(new EntityMetadata("G")); HypermediaValidator v = HypermediaValidator.createValidator(sm, metadata); String result = v.graph(); System.out.println("DOTNoContent: \n" + result); assertEquals(expected, result); } @Test public void testDOT205ResetContent() { String expected = "digraph G {\n" + " Ginitial[shape=circle, width=.25, label=\"\", color=black, style=filled]\n" + " Gexists[label=\"G.exists /entities/{id}\"]\n" + " Gdeleted[label=\"G.deleted /entities/{id}\"]\n" + " Ginitial->Gexists[label=\"*GET /entities/{id}\"]\n" + " Ginitial->Gdeleted[label=\"*DELETE /entities/{id}\"]\n" + " final[shape=circle, width=.25, label=\"\", color=black, style=filled, peripheries=2]\n Gexists->final[label=\"\"]\n Gdeleted->Ginitial[style=\"dotted\"]\n}"; String ENTITY_NAME = "G"; CollectionResourceState initial = new CollectionResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/entities"); ResourceState exists = new ResourceState(initial, "exists", new ArrayList<Action>(), "/{id}"); ResourceState deleted = new ResourceState(initial, "deleted", new ArrayList<Action>(), "/{id}"); initial.addTransition(new Transition.Builder().flags(Transition.FOR_EACH).method("GET").target(exists).build()); // add an auto transition from deleted state to a different state deleted.addTransition(new Transition.Builder().flags(Transition.AUTO).target(initial).build()); // 205, as the auto transition is to the same state we expect to see a 205 (Reset Content) at runtime initial.addTransition(new Transition.Builder().flags(Transition.FOR_EACH).method("DELETE").target(deleted).build()); ResourceStateMachine sm = new ResourceStateMachine(initial); Metadata metadata = new Metadata(""); metadata.setEntityMetadata(new EntityMetadata("G")); HypermediaValidator v = HypermediaValidator.createValidator(sm, metadata); String result = v.graph(); System.out.println("DOTResetContent: \n" + result); assertEquals(expected, result); } @Test public void testDOT303SeeOther() { String expected = "digraph G {\n" + " Ginitial[shape=circle, width=.25, label=\"\", color=black, style=filled]\n" + " Gexists[label=\"G.exists /entities/{id}\"]\n" + " Gdeleted[label=\"G.deleted /entities/{id}\"]\n" + " Ginitial->Gexists[label=\"*GET /entities/{id}\"]\n" + " Gexists->Gdeleted[label=\"DELETE /entities/{id}\"]\n" + " Gdeleted->Ginitial[style=\"dotted\"]\n}"; String ENTITY_NAME = "G"; CollectionResourceState initial = new CollectionResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/entities"); ResourceState exists = new ResourceState(initial, "exists", new ArrayList<Action>(), "/{id}"); ResourceState deleted = new ResourceState(initial, "deleted", new ArrayList<Action>(), "/{id}"); initial.addTransition(new Transition.Builder().flags(Transition.FOR_EACH).method("GET").target(exists).build()); // add an auto transition from deleted state to a different state deleted.addTransition(new Transition.Builder().flags(Transition.AUTO).target(initial).build()); // 303, as the auto transition is to a different state we expect to see a 303 (Redirect) at runtime exists.addTransition(new Transition.Builder().method("DELETE").target(deleted).build()); ResourceStateMachine sm = new ResourceStateMachine(initial); Metadata metadata = new Metadata(""); metadata.setEntityMetadata(new EntityMetadata("G")); HypermediaValidator v = HypermediaValidator.createValidator(sm, metadata); String result = v.graph(); System.out.println("DOTSeeOther: \n" + result); assertEquals(expected, result); } @Test public void testDOTMultipleFinalStates() { String expected = "digraph CRUD_ENTITY {\n" + " CRUD_ENTITYinitial[shape=circle, width=.25, label=\"\", color=black, style=filled]\n" + " CRUD_ENTITYexists[label=\"CRUD_ENTITY.exists /\"]\n" + " CRUD_ENTITYdeleted[label=\"CRUD_ENTITY.deleted /\"]\n" + " CRUD_ENTITYarchived[label=\"CRUD_ENTITY.archived /archived\"]\n" + " CRUD_ENTITYinitial->CRUD_ENTITYexists[label=\"PUT /\"]\n" + " CRUD_ENTITYexists->CRUD_ENTITYarchived[label=\"PUT /archived\"]\n" + " CRUD_ENTITYexists->CRUD_ENTITYdeleted[label=\"DELETE /\"]\n" + " final[shape=circle, width=.25, label=\"\", color=black, style=filled, peripheries=2]\n" + " CRUD_ENTITYdeleted->final[label=\"\"]\n" + " final1[shape=circle, width=.25, label=\"\", color=black, style=filled, peripheries=2]\n" + " CRUD_ENTITYarchived->final1[label=\"\"]\n}"; String ENTITY_NAME = "CRUD_ENTITY"; ResourceState initial = new ResourceState(ENTITY_NAME, "initial", new ArrayList<Action>(), "/"); ResourceState exists = new ResourceState(initial, "exists", new ArrayList<Action>(), null); ResourceState archived = new ResourceState(ENTITY_NAME, "archived", new ArrayList<Action>(), "/archived"); ResourceState deleted = new ResourceState(initial, "deleted", new ArrayList<Action>(), null); initial.addTransition(new Transition.Builder().method("PUT").target(exists).build()); exists.addTransition(new Transition.Builder().method("PUT").target(archived).build()); exists.addTransition(new Transition.Builder().method("DELETE").target(deleted).build()); ResourceStateMachine sm = new ResourceStateMachine(initial); Metadata metadata = new Metadata(""); metadata.setEntityMetadata(new EntityMetadata("CRUD_ENTITY")); HypermediaValidator v = HypermediaValidator.createValidator(sm, metadata); String result = v.graph(); assertEquals(expected, result); } @Test public void testDOTTransitionToStateMachine() { ResourceState home = new ResourceState("SERVICE_ROOT", "home", new ArrayList<Action>(), "/"); ResourceStateMachine processSM = getProcessSM(); home.addTransition("GET", processSM); ResourceStateMachine serviceDocumentSM = new ResourceStateMachine(home); Metadata metadata = new Metadata(""); metadata.setEntityMetadata(new EntityMetadata("SERVICE_ROOT")); String dotGraph = HypermediaValidator.createValidator(serviceDocumentSM, metadata).graph(); System.out.println("DOTTransitionToStateMachine: \n" + dotGraph); // we cannot directly compared the produced string with the expected one because the generation of the DOT graph // relies on getting a Collection from the ResourceStateMachine, so no order can be assumed String expectedSubstr1 = "digraph SERVICE_ROOT {\n"; assertTrue(dotGraph.contains(expectedSubstr1)); String expectedSubstr2 = " SERVICE_ROOThome[shape=circle, width=.25, label=\"\", color=black, style=filled]\n"; assertTrue(dotGraph.contains(expectedSubstr2)); String expectedSubstr3 = " taskcomplete[label=\"task.complete /completed\"]\n"; assertTrue(dotGraph.contains(expectedSubstr3)); String expectedSubstr4 = " taskacquired[label=\"task.acquired /acquired\"]\n"; assertTrue(dotGraph.contains(expectedSubstr4)); String expectedSubstr5 = " taskabandoned[label=\"task.abandoned /acquired\"]\n"; assertTrue(dotGraph.contains(expectedSubstr5)); String expectedSubstr6 = " processtaskAvailable[label=\"process.taskAvailable /processes/nextTask\"]\n"; assertTrue(dotGraph.contains(expectedSubstr6)); String expectedSubstr7 = " processprocesses[label=\"process.processes /processes\"]\n"; assertTrue(dotGraph.contains(expectedSubstr7)); String expectedSubstr8 = " processnew[label=\"process.new /processes/new\"]\n"; assertTrue(dotGraph.contains(expectedSubstr8)); String expectedSubstr9 = " processinitialProcess[label=\"process.initialProcess /processes/{id}\"]\n"; assertTrue(dotGraph.contains(expectedSubstr9)); String expectedSubstr10 = " processcompletedProcess[label=\"process.completedProcess /processes/{id}\"]\n"; assertTrue(dotGraph.contains(expectedSubstr10)); String expectedSubstr11 = " final[shape=circle, width=.25, label=\"\", color=black, style=filled, peripheries=2]\n"; assertTrue(dotGraph.contains(expectedSubstr11)); String expectedSubstr12 = " taskcomplete->final[label=\"\"]\n"; assertTrue(dotGraph.contains(expectedSubstr12)); String expectedSubstr13 = " taskacquired->taskabandoned[label=\"DELETE /acquired\"]\n"; assertTrue(dotGraph.contains(expectedSubstr13)); String expectedSubstr14 = " taskacquired->taskcomplete[label=\"PUT /completed\"]\n"; assertTrue(dotGraph.contains(expectedSubstr14)); String expectedSubstr15 = " final1[shape=circle, width=.25, label=\"\", color=black, style=filled, peripheries=2]\n"; assertTrue(dotGraph.contains(expectedSubstr15)); String expectedSubstr16 = " taskabandoned->final1[label=\"\"]\n"; assertTrue(dotGraph.contains(expectedSubstr16)); String expectedSubstr17 = " processtaskAvailable->taskacquired[label=\"PUT /acquired\"]\n"; assertTrue(dotGraph.contains(expectedSubstr17)); String expectedSubstr18 = " processprocesses->processnew[label=\"POST /processes/new\"]\n"; assertTrue(dotGraph.contains(expectedSubstr18)); String expectedSubstr19 = " processnew->processinitialProcess[label=\"PUT /processes/{id}\"]\n"; assertTrue(dotGraph.contains(expectedSubstr19)); String expectedSubstr20 = " processinitialProcess->processtaskAvailable[label=\"GET /processes/nextTask\"]\n"; assertTrue(dotGraph.contains(expectedSubstr20)); String expectedSubstr21 = " processinitialProcess->processcompletedProcess[label=\"DELETE /processes/{id}\"]\n"; assertTrue(dotGraph.contains(expectedSubstr21)); String expectedSubstr22 = " final2[shape=circle, width=.25, label=\"\", color=black, style=filled, peripheries=2]\n"; assertTrue(dotGraph.contains(expectedSubstr22)); String expectedSubstr23 = " processcompletedProcess->final2[label=\"\"]\n"; assertTrue(dotGraph.contains(expectedSubstr23)); String expectedSubstr24 = " SERVICE_ROOThome->processprocesses[label=\"GET /processes\"]\n"; assertTrue(dotGraph.contains(expectedSubstr24)); String expectedSubstr25 = "}"; assertTrue(dotGraph.contains(expectedSubstr25)); // checked that all expected lines are in the return DOT graph, now I'm checking that // no other lines than the expected ones are returned assertEquals(25, dotGraph.split("\n").length); } @Test public void testDOTGraphOneLevel() { ResourceState home = new ResourceState("SERVICE_ROOT", "home", new ArrayList<Action>(), "/"); // processes ResourceStateMachine processSM = getProcessSM(); home.addTransition("GET", processSM); // notes ResourceStateMachine notesSM = new ResourceStateMachine(new ResourceState("notes", "initial", new ArrayList<Action>(), "/notes")); home.addTransition("GET", notesSM); ResourceStateMachine serviceDocumentSM = new ResourceStateMachine(home); Metadata metadata = new Metadata(""); metadata.setEntityMetadata(new EntityMetadata("SERVICE_ROOT")); String dotGraph = HypermediaValidator.createValidator(serviceDocumentSM, metadata).graphEntityNextStates(); // we cannot directly compared the produced string with the expected one because the generation of the DOT graph // relies on getting a Collection from the ResourceStateMachine, so no order can be assumed String expectedSubstr1 = "digraph SERVICE_ROOT {\n"; assertTrue(dotGraph.contains(expectedSubstr1)); String expectedSubstr2 = " SERVICE_ROOThome[shape=circle, width=.25, label=\"\", color=black, style=filled]\n"; assertTrue(dotGraph.contains(expectedSubstr2)); String expectedSubstr3 = " processprocesses[shape=square, width=.25, label=\"process.processes\"]\n"; assertTrue(dotGraph.contains(expectedSubstr3)); String expectedSubstr4 = " taskacquired[shape=square, width=.25, label=\"task.acquired\"]\n"; assertTrue(dotGraph.contains(expectedSubstr4)); String expectedSubstr5 = " notesinitial[shape=square, width=.25, label=\"notes.initial\"]\n"; assertTrue(dotGraph.contains(expectedSubstr5)); String expectedSubstr6 = " SERVICE_ROOThome->processprocesses[label=\"GET /processes\"]\n"; assertTrue(dotGraph.contains(expectedSubstr6)); String expectedSubstr7 = " SERVICE_ROOThome->notesinitial[label=\"GET /notes\"]\n"; assertTrue(dotGraph.contains(expectedSubstr7)); String expectedSubstr8 = "}"; assertTrue(dotGraph.contains(expectedSubstr8)); // checked that all expected lines are in the return DOT graph, now I'm checking that // no other lines than the expected ones are returned assertEquals(8, dotGraph.split("\n").length); } private ResourceStateMachine getProcessSM() { String PROCESS_ENTITY_NAME = "process"; // process behaviour ResourceState processes = new ResourceState(PROCESS_ENTITY_NAME, "processes", new ArrayList<Action>(), "/processes"); ResourceState newProcess = new ResourceState(PROCESS_ENTITY_NAME, "new", new ArrayList<Action>(), "/processes/new"); // create new process processes.addTransition(new Transition.Builder().method("POST").target(newProcess).build()); // Process states ResourceState processInitial = new ResourceState(PROCESS_ENTITY_NAME, "initialProcess", new ArrayList<Action>(), "/processes/{id}"); ResourceState nextTask = new ResourceState(PROCESS_ENTITY_NAME, "taskAvailable", new ArrayList<Action>(), "/processes/nextTask"); ResourceState processCompleted = new ResourceState(PROCESS_ENTITY_NAME, "completedProcess", new ArrayList<Action>(), "/processes/{id}"); // start new process newProcess.addTransition(new Transition.Builder().method("PUT").target(processInitial).build()); // do a task processInitial.addTransition(new Transition.Builder().method("GET").target(nextTask).build()); // finish the process processInitial.addTransition(new Transition.Builder().method("DELETE").target(processCompleted).build()); /* * acquire task by a PUT to the initial state of the task state machine (acquired) */ ResourceStateMachine taskSM = getTaskSM(); nextTask.addTransition("PUT", taskSM); return new ResourceStateMachine(processes); } private ResourceStateMachine getTaskSM() { String TASK_ENTITY_NAME = "task"; // Task states ResourceState taskAcquired = new ResourceState(TASK_ENTITY_NAME, "acquired", new ArrayList<Action>(), "/acquired"); ResourceState taskComplete = new ResourceState(TASK_ENTITY_NAME, "complete", new ArrayList<Action>(), "/completed"); ResourceState taskAbandoned = new ResourceState(TASK_ENTITY_NAME, "abandoned", new ArrayList<Action>(), "/acquired"); // abandon task taskAcquired.addTransition(new Transition.Builder().method("DELETE").target(taskAbandoned).build()); // complete task taskAcquired.addTransition(new Transition.Builder().method("PUT").target(taskComplete).build()); return new ResourceStateMachine(taskAcquired); } }