Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test OptaPlanner Spring Boot example via REST API #262

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions process-optaplanner-springboot/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,21 @@
</dependency>

<!-- Testing -->
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- JavaScript libraries for frontend -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,23 @@
public class Seat {

@PlanningId
private final String name;
private final int row;
private final int column;
private final SeatType seatType;
private final boolean emergencyExitRow;
private String name;
private int row;
private int column;
private SeatType seatType;
private boolean emergencyExitRow;

public Seat() {
// required by jackson-databind
}

public Seat(int row, int column, SeatType seatType, boolean emergencyExitRow) {
this.row = row;
this.column = column;
// ASCII has a nice property: The English Alphabet are placed in consecutive
// ASCII codes. So 'B' is immediately after 'A', 'C' is immediately after 'B',
// etc. So 'A' + n = nth letter of the alphabet.
// Name is row number (starting at 1) + column letter (starting at 'A').
// Name is row number (starting at 1) + column letter (starting at 'A').
this.name = (row + 1) + Character.toString((char) ('A' + column));
this.seatType = seatType;
this.emergencyExitRow = emergencyExitRow;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,92 +15,249 @@
*/
package org.kie.kogito.examples;

import static org.junit.Assert.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;
import javax.inject.Named;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.kie.kogito.Model;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.kie.kogito.examples.domain.FlightDTO;
import org.kie.kogito.examples.domain.Passenger;
import org.kie.kogito.examples.domain.PassengerDTO;
import org.kie.kogito.process.Process;
import org.kie.kogito.process.Processes;
import org.kie.kogito.process.ProcessInstance;
import org.kie.kogito.process.WorkItem;
import org.kie.kogito.process.impl.Sig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import io.restassured.RestAssured;
import io.restassured.http.ContentType;

import static io.restassured.RestAssured.given;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;

@RunWith(SpringRunner.class)
@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = FlightSeatingApplication.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
// reset spring context after each test method
public class FlightTest {

@Autowired
Processes processMaker;
private final ObjectMapper jsonMapper = new ObjectMapper();

@Test
public void runProcess() {
Process<? extends Model> process = processMaker.processById("flights");
assertNotNull(process);
@LocalServerPort
private int port;

@BeforeEach
public void setUp() {
RestAssured.port = port;
}

@Test
public void runProcess() throws JsonProcessingException {
FlightDTO flightParams = new FlightDTO();
flightParams.setDepartureDateTime(LocalDateTime.now().toString());
flightParams.setDestination("");
flightParams.setOrigin("");
flightParams.setDestination("B");
flightParams.setOrigin("A");
flightParams.setSeatColumnSize(10);
flightParams.setSeatRowSize(6);

Model m = process.createModel();
Map<String, Object> parameters = new HashMap<>();
parameters.put("params", flightParams);
m.fromMap(parameters);
String flightId = createFlight(flightParams);

ProcessInstance<?> processInstance = process.createInstance(m);
processInstance.start();
assertEquals(ProcessInstance.STATE_ACTIVE, processInstance.status());
// add new passenger
processInstance.send(Sig.of("newPassengerRequest", new PassengerDTO("john", "NONE", false, false, null)));
PassengerDTO passenger = new PassengerDTO("john", "WINDOW", false, false, null);
addPassenger(flightId, passenger);

List<WorkItem> tasks = processInstance.workItems();
Map<String, String> tasks = getTasks(flightId);
assertEquals(2, tasks.size());
// approve passenger
Map<String, Object> result = new HashMap<>();
result.put("isPassengerApproved", true);
processInstance.completeWorkItem(tasks.get(1).getId(), result);
assertThat(tasks.values(), hasItem("approveDenyPassenger"));
assertThat(tasks.values(), hasItem("finalizePassengerList"));

String approveDenyPassengerTask = tasks.entrySet().stream()
.filter(e -> e.getValue().equals("approveDenyPassenger"))
.map(Map.Entry::getKey).findAny().get();

approvePassenger(flightId, approveDenyPassengerTask, passenger.getName());

// close the passenger list so no more passengers can be added
result = new HashMap<>();
result.put("isPassengerListFinalized", true);
processInstance.completeWorkItem(tasks.get(0).getId(), result);
String finalizePassengerListTask = tasks.entrySet().stream()
.filter(e -> e.getValue().equals("finalizePassengerList"))
.map(Map.Entry::getKey).findAny().get();

finalizePassengerList(flightId, finalizePassengerListTask);

String finalizeSeatAssignmentTask = waitForNextTaskAfterAssignment(flightId, 10);

try {
Thread.sleep(10000);
} catch (Exception e) {
verifyFlightIsAssigned(flightId);

finalizeSeatAssignment(flightId, finalizeSeatAssignmentTask);

assertNotMoreFlights(flightId);
}

private String waitForNextTaskAfterAssignment(String flightId, int timeoutSeconds) {
final long stepMillis = 1000L;
long waitingSpentMillis = 0L;
Map<String, String> tasks;

while((tasks = getTasks(flightId)).isEmpty()) {
if (waitingSpentMillis > timeoutSeconds * 1000) {
throw new RuntimeException("Waiting for seat assignment has exceeded " + timeoutSeconds + " seconds (timeout).");
}

waitingSpentMillis += stepMillis;
try {
Thread.sleep(stepMillis);
} catch (Exception e) {
System.out.println("Interrupted waiting.");
e.printStackTrace();
}
}

tasks = processInstance.workItems();
return tasks.keySet().iterator().next();
}

private String createFlight(FlightDTO flightParams) throws JsonProcessingException {
Map<String, Object> parameters = new HashMap<>();
parameters.put("params", flightParams);

final String id = given()
.body(jsonMapper.writeValueAsString(parameters))
.contentType(ContentType.JSON)
.when()
.post("/rest/flights")
.then()
.statusCode(200)
.body("params.departureDateTime", is(flightParams.getDepartureDateTime()))
.body("params.origin", is(flightParams.getOrigin()))
.body("params.destination", is(flightParams.getDestination()))
.body("params.seatRowSize", is(flightParams.getSeatRowSize()))
.body("params.seatColumnSize", is(flightParams.getSeatColumnSize()))
.extract()
.body()
.path("id");

given()
.contentType(ContentType.JSON)
.when()
.get("/rest/flights/" + id)
.then()
.statusCode(200)
.body("params.departureDateTime", is(flightParams.getDepartureDateTime()))
.body("params.origin", is(flightParams.getOrigin()))
.body("params.destination", is(flightParams.getDestination()))
.body("params.seatRowSize", is(flightParams.getSeatRowSize()))
.body("params.seatColumnSize", is(flightParams.getSeatColumnSize()));

return id;
}

private void addPassenger(String flightId, PassengerDTO passenger) throws JsonProcessingException {
given()
.body(jsonMapper.writeValueAsString(passenger))
.contentType(ContentType.JSON)
.when()
.post("/rest/flights/" + flightId + "/newPassengerRequest")
.then()
.statusCode(200);
}

private Map<String, String> getTasks(String flightId) {
Map<String, String> tasks = given()
.contentType(ContentType.JSON)
.when()
.get("/rest/flights/" + flightId + "/tasks")
.then()
.extract()
.body()
.jsonPath()
.getMap("");

return tasks;
}

private void approvePassenger(String flightId, String approveDenyPassengerTask, String passengerName)
throws JsonProcessingException {
// check the passenger name
given()
.contentType(ContentType.JSON)
.when()
.get("/rest/flights/" + flightId + "/approveDenyPassenger/" + approveDenyPassengerTask)
.then()
.statusCode(200)
.body("passenger.name", is(passengerName));

// approve passenger
Map<String, Object> parameters = new HashMap<>();
parameters.put("isPassengerApproved", true);
given()
.body(jsonMapper.writeValueAsString(parameters))
.contentType(ContentType.JSON)
.when()
.post("/rest/flights/" + flightId + "/approveDenyPassenger/" + approveDenyPassengerTask)
.then()
.statusCode(200);
}

private void finalizePassengerList(String flightId, String finalizePassengerListTask) throws JsonProcessingException {
Map<String, Object> parameters = new HashMap<>();
parameters.put("isPassengerListFinalized", true);
given()
.body(jsonMapper.writeValueAsString(parameters))
.contentType(ContentType.JSON)
.when()
.post("/rest/flights/" + flightId + "/finalizePassengerList/" + finalizePassengerListTask)
.then()
.statusCode(200);
}

private void verifyFlightIsAssigned(String flightId) {
List<Passenger> passengerList = given()
.contentType(ContentType.JSON)
.when()
.get("/rest/flights/" + flightId)
.then()
.statusCode(200)
.extract()
.body()
.jsonPath()
.getList("flight.passengerList", Passenger.class);

Assertions.assertNotNull(passengerList.get(0).getSeat());
}

private void finalizeSeatAssignment(String flightId, String finializeSeatAssignmentTask) throws JsonProcessingException {
given()
.body(jsonMapper.writeValueAsString(Collections.EMPTY_MAP))
.contentType(ContentType.JSON)
.when()
.post("/rest/flights/" + flightId + "/finalizeSeatAssignment/" + finializeSeatAssignmentTask)
.then()
.statusCode(200);
}

assertEquals(1, tasks.size());
// then complete the flight
processInstance.completeWorkItem(tasks.get(0).getId(), null);
// flight process is done
assertEquals(ProcessInstance.STATE_COMPLETED, processInstance.status());
private void assertNotMoreFlights(String flightId) {
given()
.contentType(ContentType.JSON)
.when()
.get("/rest/flights")
.then()
.statusCode(200)
.body("", hasSize(0));

Model resultData = (Model) processInstance.variables();
assertEquals(5, resultData.toMap().size());
given()
.contentType(ContentType.JSON)
.when()
.get("/rest/flights/" + flightId)
.then()
// I would expect a 404, not a 204, for a missing process
.statusCode(204);
}
}