S8 apply Palantir formatting to maintained Java sources
All checks were successful
Build, test and publish / verify (push) Successful in 1m39s
All checks were successful
Build, test and publish / verify (push) Successful in 1m39s
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
package nl.agilecc.agiledancers;
|
||||
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import com.tngtech.archunit.core.domain.JavaModifier;
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
|
||||
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
|
||||
|
||||
import com.tngtech.archunit.core.domain.JavaModifier;
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
|
||||
@AnalyzeClasses(packages = "nl.agilecc.agiledancers", importOptions = ImportOption.DoNotIncludeTests.class)
|
||||
class ArchitectureTest {
|
||||
private static final String ROOT = "nl.agilecc.agiledancers";
|
||||
@@ -18,85 +19,128 @@ class ArchitectureTest {
|
||||
// Exclude Lombok builders and compiler-generated enum-switch helper classes.
|
||||
@ArchTest
|
||||
static final ArchRule handwritten_types_are_top_level = classes()
|
||||
.that().areNotAnnotatedWith("lombok.Generated")
|
||||
.and().doNotHaveModifier(JavaModifier.SYNTHETIC)
|
||||
.should().beTopLevelClasses();
|
||||
.that()
|
||||
.areNotAnnotatedWith("lombok.Generated")
|
||||
.and()
|
||||
.doNotHaveModifier(JavaModifier.SYNTHETIC)
|
||||
.should()
|
||||
.beTopLevelClasses();
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule service_records_reside_in_service_model = classes()
|
||||
.that().resideInAPackage(ROOT + ".service..")
|
||||
.and().areRecords()
|
||||
.should().resideInAPackage(ROOT + ".service.model");
|
||||
.that()
|
||||
.resideInAPackage(ROOT + ".service..")
|
||||
.and()
|
||||
.areRecords()
|
||||
.should()
|
||||
.resideInAPackage(ROOT + ".service.model");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule service_interfaces_reside_in_service_port = classes()
|
||||
.that().resideInAPackage(ROOT + ".service..")
|
||||
.and().areInterfaces()
|
||||
.should().resideInAPackage(ROOT + ".service.port");
|
||||
.that()
|
||||
.resideInAPackage(ROOT + ".service..")
|
||||
.and()
|
||||
.areInterfaces()
|
||||
.should()
|
||||
.resideInAPackage(ROOT + ".service.port");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule scheduling_port_uses_domain_types = classes()
|
||||
.that().resideInAPackage(ROOT + ".service.port..")
|
||||
.should().onlyDependOnClassesThat()
|
||||
.that()
|
||||
.resideInAPackage(ROOT + ".service.port..")
|
||||
.should()
|
||||
.onlyDependOnClassesThat()
|
||||
.resideInAnyPackage(ROOT + ".service.port..", ROOT + ".model..", "java..");
|
||||
|
||||
// API model classes and records use the Request, Response, View, Dto or DTO suffix.
|
||||
@ArchTest
|
||||
static final ArchRule api_models_reside_in_the_api_model_package = classes()
|
||||
.that().haveNameMatching(".*(Request|Response|View|Dto|DTO)$")
|
||||
.should().resideInAPackage(API_MODEL);
|
||||
.that()
|
||||
.haveNameMatching(".*(Request|Response|View|Dto|DTO)$")
|
||||
.should()
|
||||
.resideInAPackage(API_MODEL);
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule rest_records_reside_in_the_api_model_package = classes()
|
||||
.that().resideInAPackage(ROOT + ".rest..")
|
||||
.and().areRecords()
|
||||
.should().resideInAPackage(API_MODEL);
|
||||
.that()
|
||||
.resideInAPackage(ROOT + ".rest..")
|
||||
.and()
|
||||
.areRecords()
|
||||
.should()
|
||||
.resideInAPackage(API_MODEL);
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule api_models_are_public_top_level_types = classes()
|
||||
.that().resideInAPackage(API_MODEL + "..")
|
||||
.should().bePublic()
|
||||
.andShould().beTopLevelClasses()
|
||||
.andShould().haveNameMatching(".*(Request|Response|View|Dto|DTO)$");
|
||||
.that()
|
||||
.resideInAPackage(API_MODEL + "..")
|
||||
.should()
|
||||
.bePublic()
|
||||
.andShould()
|
||||
.beTopLevelClasses()
|
||||
.andShould()
|
||||
.haveNameMatching(".*(Request|Response|View|Dto|DTO)$");
|
||||
|
||||
// The existing services depend on concrete repositories. This rule preserves that
|
||||
// arrangement until repository ports and dependency injection are introduced.
|
||||
@ArchTest
|
||||
static final ArchRule layer_dependencies = layeredArchitecture()
|
||||
.consideringOnlyDependenciesInLayers()
|
||||
.layer("Model").definedBy(ROOT + ".model..")
|
||||
.layer("Services").definedBy(ROOT + ".service..")
|
||||
.layer("Repositories").definedBy(ROOT + ".repository..")
|
||||
.optionalLayer("REST").definedBy(ROOT + ".rest..")
|
||||
.optionalLayer("Solver").definedBy(ROOT + ".solver..")
|
||||
.whereLayer("Model").mayNotAccessAnyLayer()
|
||||
.whereLayer("Services").mayOnlyAccessLayers("Model", "Repositories")
|
||||
.whereLayer("Repositories").mayOnlyAccessLayers("Model")
|
||||
.whereLayer("REST").mayOnlyAccessLayers("Services", "Model")
|
||||
.whereLayer("Solver").mayOnlyAccessLayers("Services", "Model");
|
||||
.layer("Model")
|
||||
.definedBy(ROOT + ".model..")
|
||||
.layer("Services")
|
||||
.definedBy(ROOT + ".service..")
|
||||
.layer("Repositories")
|
||||
.definedBy(ROOT + ".repository..")
|
||||
.optionalLayer("REST")
|
||||
.definedBy(ROOT + ".rest..")
|
||||
.optionalLayer("Solver")
|
||||
.definedBy(ROOT + ".solver..")
|
||||
.whereLayer("Model")
|
||||
.mayNotAccessAnyLayer()
|
||||
.whereLayer("Services")
|
||||
.mayOnlyAccessLayers("Model", "Repositories")
|
||||
.whereLayer("Repositories")
|
||||
.mayOnlyAccessLayers("Model")
|
||||
.whereLayer("REST")
|
||||
.mayOnlyAccessLayers("Services", "Model")
|
||||
.whereLayer("Solver")
|
||||
.mayOnlyAccessLayers("Services", "Model");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule production_classes_have_an_architectural_home = classes()
|
||||
.should().resideInAnyPackage(ROOT + ".model..", ROOT + ".service..",
|
||||
ROOT + ".repository..", ROOT + ".rest..", ROOT + ".solver..");
|
||||
.should()
|
||||
.resideInAnyPackage(
|
||||
ROOT + ".model..",
|
||||
ROOT + ".service..",
|
||||
ROOT + ".repository..",
|
||||
ROOT + ".rest..",
|
||||
ROOT + ".solver..");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule model_is_independent_of_frameworks = classes()
|
||||
.that().resideInAPackage(ROOT + ".model..")
|
||||
.should().onlyDependOnClassesThat()
|
||||
.that()
|
||||
.resideInAPackage(ROOT + ".model..")
|
||||
.should()
|
||||
.onlyDependOnClassesThat()
|
||||
.resideInAnyPackage(ROOT + ".model..", "java..", "lombok..");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule ortools_is_encapsulated_in_solver = noClasses()
|
||||
.that().resideOutsideOfPackage(ROOT + ".solver..")
|
||||
.should().dependOnClassesThat().resideInAPackage("com.google.ortools..");
|
||||
.that()
|
||||
.resideOutsideOfPackage(ROOT + ".solver..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAPackage("com.google.ortools..");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule http_api_is_encapsulated_in_rest = noClasses()
|
||||
.that().resideOutsideOfPackage(ROOT + ".rest..")
|
||||
.should().dependOnClassesThat().resideInAnyPackage("jakarta.ws.rs..", "org.jboss.resteasy..");
|
||||
.that()
|
||||
.resideOutsideOfPackage(ROOT + ".rest..")
|
||||
.should()
|
||||
.dependOnClassesThat()
|
||||
.resideInAnyPackage("jakarta.ws.rs..", "org.jboss.resteasy..");
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule packages_are_free_of_cycles = slices()
|
||||
.matching(ROOT + ".(*)..").should().beFreeOfCycles();
|
||||
static final ArchRule packages_are_free_of_cycles =
|
||||
slices().matching(ROOT + ".(*)..").should().beFreeOfCycles();
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
package nl.agilecc.agiledancers;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@QuarkusTest
|
||||
class HealthTest {
|
||||
@Test
|
||||
void applicationIsReady() {
|
||||
given()
|
||||
.when().get("/q/health/ready")
|
||||
.then().statusCode(200)
|
||||
.body("status", equalTo("UP"));
|
||||
given().when().get("/q/health/ready").then().statusCode(200).body("status", equalTo("UP"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +1,80 @@
|
||||
package nl.agilecc.agiledancers;
|
||||
|
||||
import nl.agilecc.agiledancers.model.scheduling.SchedulingResult;
|
||||
import nl.agilecc.agiledancers.service.model.*;
|
||||
import nl.agilecc.agiledancers.service.port.SchedulingEngine;
|
||||
import nl.agilecc.agiledancers.model.*;
|
||||
import nl.agilecc.agiledancers.model.specifications.*;
|
||||
import nl.agilecc.agiledancers.service.*;
|
||||
import nl.agilecc.agiledancers.solver.OrToolsSchedulingEngine;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import nl.agilecc.agiledancers.model.*;
|
||||
import nl.agilecc.agiledancers.model.scheduling.SchedulingResult;
|
||||
import nl.agilecc.agiledancers.model.specifications.*;
|
||||
import nl.agilecc.agiledancers.service.*;
|
||||
import nl.agilecc.agiledancers.service.model.*;
|
||||
import nl.agilecc.agiledancers.solver.OrToolsSchedulingEngine;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchedulingTest {
|
||||
private final SchoolManagementService service = new SchoolManagementService();
|
||||
private final long school = service.createSchool("Studio").getId();
|
||||
|
||||
private List<DayPart> parts(int n) {
|
||||
return java.util.stream.IntStream.range(0, n).mapToObj(i -> DayPart.builder()
|
||||
.date(LocalDate.of(2027, 1, 1).plusDays(i)).type(DayPartType.AFTERNOON).danceGroups(List.of()).build()).toList();
|
||||
return java.util.stream.IntStream.range(0, n)
|
||||
.mapToObj(i -> DayPart.builder()
|
||||
.date(LocalDate.of(2027, 1, 1).plusDays(i))
|
||||
.type(DayPartType.AFTERNOON)
|
||||
.danceGroups(List.of())
|
||||
.build())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private DanceGroup group(DanceGroupType type) {
|
||||
return service.createGroup(school, "Group", type, 0, 99, Set.of());
|
||||
}
|
||||
|
||||
private void run(Show show) {
|
||||
var input = service.beginScheduling(school, show.getId());
|
||||
try { service.finishScheduling(school, input, new OrToolsSchedulingEngine().solve(input)); }
|
||||
finally { service.endScheduling(show.getId()); }
|
||||
try {
|
||||
service.finishScheduling(school, input, new OrToolsSchedulingEngine().solve(input));
|
||||
} finally {
|
||||
service.endScheduling(show.getId());
|
||||
}
|
||||
}
|
||||
@Test void exactAppearancesAndOptimalBalanceIncludingEmptyParts() {
|
||||
|
||||
@Test
|
||||
void exactAppearancesAndOptimalBalanceIncludingEmptyParts() {
|
||||
group(DanceGroupType.PRE_SCHOOLER);
|
||||
var show = service.createShow(school, "Show", parts(6), null);
|
||||
run(show);
|
||||
var loads = show.getDayParts().stream().mapToInt(p -> p.getDanceGroups().size()).summaryStatistics();
|
||||
assertEquals(1, loads.getSum()); assertEquals(0, loads.getMin()); assertEquals(1, loads.getMax());
|
||||
var loads = show.getDayParts().stream()
|
||||
.mapToInt(p -> p.getDanceGroups().size())
|
||||
.summaryStatistics();
|
||||
assertEquals(1, loads.getSum());
|
||||
assertEquals(0, loads.getMin());
|
||||
assertEquals(1, loads.getMax());
|
||||
for (var type : DanceGroupType.values()) group(type);
|
||||
run(show);
|
||||
for (var group : service.groups(school)) assertEquals(show.getAppearances().get(group.getType()).longValue(),
|
||||
show.getDayParts().stream().filter(p -> p.getDanceGroups().contains(group)).count());
|
||||
loads = show.getDayParts().stream().mapToInt(p -> p.getDanceGroups().size()).summaryStatistics();
|
||||
for (var group : service.groups(school))
|
||||
assertEquals(
|
||||
show.getAppearances().get(group.getType()).longValue(),
|
||||
show.getDayParts().stream()
|
||||
.filter(p -> p.getDanceGroups().contains(group))
|
||||
.count());
|
||||
loads = show.getDayParts().stream()
|
||||
.mapToInt(p -> p.getDanceGroups().size())
|
||||
.summaryStatistics();
|
||||
assertTrue(loads.getMax() - loads.getMin() <= 1);
|
||||
var snapshot = service.exportSchool(school);
|
||||
assertEquals(snapshot, SchoolSnapshot.of(snapshot.restore()));
|
||||
}
|
||||
@Test void defaultsInitialConfigurationAndPreservationOnEdits() {
|
||||
|
||||
@Test
|
||||
void defaultsInitialConfigurationAndPreservationOnEdits() {
|
||||
var show = service.createShow(school, "Show", List.of(), null);
|
||||
assertTrue(show.getAppearances().isEmpty());
|
||||
service.updateShow(school, show.getId(), "Show", parts(3), null);
|
||||
assertEquals(3, show.getAppearances().get(DanceGroupType.PRE_EDUCATION));
|
||||
assertEquals(1, show.getAppearances().get(DanceGroupType.PRE_SCHOOLER));
|
||||
var counts = new EnumMap<>(show.getAppearances()); counts.put(DanceGroupType.PRE_SCHOOLER, 2);
|
||||
var counts = new EnumMap<>(show.getAppearances());
|
||||
counts.put(DanceGroupType.PRE_SCHOOLER, 2);
|
||||
service.updateShow(school, show.getId(), "Show", parts(4), counts);
|
||||
assertEquals(counts, show.getAppearances());
|
||||
assertThrows(ManagementException.class, () -> service.updateShow(school, show.getId(), "Show", parts(1), null));
|
||||
@@ -58,19 +84,25 @@ class SchedulingTest {
|
||||
service.updateShow(school, show.getId(), "Show", parts(4), null);
|
||||
assertEquals(counts, show.getAppearances());
|
||||
}
|
||||
@Test void rejectsDuplicatePartsMissingCountsAndOutOfRangeCounts() {
|
||||
|
||||
@Test
|
||||
void rejectsDuplicatePartsMissingCountsAndOutOfRangeCounts() {
|
||||
var duplicate = List.of(parts(1).getFirst(), parts(1).getFirst());
|
||||
assertThrows(ManagementException.class, () -> service.createShow(school, "Show", duplicate, null));
|
||||
assertThrows(ManagementException.class, () -> service.createShow(school, "Show", parts(7), null));
|
||||
assertThrows(ManagementException.class, () -> service.createShow(school, "Show", parts(2), Map.of()));
|
||||
var counts = ShowConfiguration.defaults(2); counts.put(DanceGroupType.REGULAR, 3);
|
||||
var counts = ShowConfiguration.defaults(2);
|
||||
counts.put(DanceGroupType.REGULAR, 3);
|
||||
assertThrows(ManagementException.class, () -> service.createShow(school, "Show", parts(2), counts));
|
||||
counts.put(DanceGroupType.REGULAR, 0);
|
||||
assertThrows(ManagementException.class, () -> service.createShow(school, "Show", parts(2), counts));
|
||||
}
|
||||
@Test void duplicateRunsStaleResultsAndMalformedResultsPreserveAssignments() {
|
||||
|
||||
@Test
|
||||
void duplicateRunsStaleResultsAndMalformedResultsPreserveAssignments() {
|
||||
var group = group(DanceGroupType.REGULAR);
|
||||
var show = service.createShow(school, "Show", parts(2), null); run(show);
|
||||
var show = service.createShow(school, "Show", parts(2), null);
|
||||
run(show);
|
||||
var before = show.getDayParts().stream().map(DayPart::getDanceGroups).toList();
|
||||
var input = service.beginScheduling(school, show.getId());
|
||||
assertThrows(ManagementException.class, () -> service.beginScheduling(school, show.getId()));
|
||||
@@ -79,18 +111,25 @@ class SchedulingTest {
|
||||
assertDoesNotThrow(() -> service.finishScheduling(school, input, result));
|
||||
group(DanceGroupType.PRE_SCHOOLER);
|
||||
assertThrows(ManagementException.class, () -> service.finishScheduling(school, input, result));
|
||||
assertEquals(before, show.getDayParts().stream().map(DayPart::getDanceGroups).toList());
|
||||
assertEquals(
|
||||
before, show.getDayParts().stream().map(DayPart::getDanceGroups).toList());
|
||||
service.endScheduling(show.getId());
|
||||
var fresh = service.beginScheduling(school, show.getId());
|
||||
assertThrows(ManagementException.class, () -> service.finishScheduling(school, fresh,
|
||||
new SchedulingResult(false, List.of(List.of(), List.of()))));
|
||||
assertEquals(before, show.getDayParts().stream().map(DayPart::getDanceGroups).toList());
|
||||
assertThrows(
|
||||
ManagementException.class,
|
||||
() -> service.finishScheduling(
|
||||
school, fresh, new SchedulingResult(false, List.of(List.of(), List.of()))));
|
||||
assertEquals(
|
||||
before, show.getDayParts().stream().map(DayPart::getDanceGroups).toList());
|
||||
// Feasible results are accepted without requiring proof of optimality.
|
||||
var valid = new OrToolsSchedulingEngine().solve(fresh);
|
||||
assertDoesNotThrow(() -> service.finishScheduling(school, fresh, new SchedulingResult(false, valid.assignments())));
|
||||
assertDoesNotThrow(
|
||||
() -> service.finishScheduling(school, fresh, new SchedulingResult(false, valid.assignments())));
|
||||
service.endScheduling(show.getId());
|
||||
}
|
||||
@Test void staleDaypartCountChangesAndDeletedShowsAreRejected() {
|
||||
|
||||
@Test
|
||||
void staleDaypartCountChangesAndDeletedShowsAreRejected() {
|
||||
group(DanceGroupType.REGULAR);
|
||||
var show = service.createShow(school, "Show", parts(2), null);
|
||||
var input = service.beginScheduling(school, show.getId());
|
||||
@@ -101,7 +140,9 @@ class SchedulingTest {
|
||||
assertThrows(ManagementException.class, () -> service.finishScheduling(school, input, result));
|
||||
service.endScheduling(show.getId());
|
||||
}
|
||||
@Test void needsGroupsAndDayparts() {
|
||||
|
||||
@Test
|
||||
void needsGroupsAndDayparts() {
|
||||
var show = service.createShow(school, "Show", parts(1), null);
|
||||
assertThrows(ManagementException.class, () -> service.beginScheduling(school, show.getId()));
|
||||
group(DanceGroupType.REGULAR);
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
package nl.agilecc.agiledancers;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import io.restassured.response.ValidatableResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.Map;
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@QuarkusTest
|
||||
class SchoolManagementResourceTest {
|
||||
private static final String SCHOOLS = "/api/dance-schools";
|
||||
private static final Map<String, Object> DANCER = Map.of("name", "Sam", "birthDate", "2010-05-12");
|
||||
private static final Map<String, Object> GROUP = Map.of("category", "Teens", "type", "REGULAR",
|
||||
"startAge", 12, "endAge", 18, "styles", new String[]{"HIPHOP"});
|
||||
private static final Map<String, Object> SHOW = Map.of("name", "Annual show", "dayParts", new Object[]{Map.of("date", "2027-06-12", "type", "AFTERNOON")});
|
||||
private static final Map<String, Object> GROUP = Map.of(
|
||||
"category", "Teens", "type", "REGULAR", "startAge", 12, "endAge", 18, "styles", new String[] {"HIPHOP"});
|
||||
private static final Map<String, Object> SHOW =
|
||||
Map.of("name", "Annual show", "dayParts", new Object[] {Map.of("date", "2027-06-12", "type", "AFTERNOON")});
|
||||
|
||||
@Test
|
||||
void managesSchoolAndItsResources() {
|
||||
@@ -27,37 +29,64 @@ class SchoolManagementResourceTest {
|
||||
long dancerId = create(school + "/dancers", DANCER);
|
||||
String dancer = school + "/dancers/" + dancerId;
|
||||
put(dancer, Map.of("name", "Sam updated", "birthDate", "2011-06-13"))
|
||||
.statusCode(200).body("birthDate", equalTo("2011-06-13"));
|
||||
.statusCode(200)
|
||||
.body("birthDate", equalTo("2011-06-13"));
|
||||
given().get(dancer).then().statusCode(200).body("name", equalTo("Sam updated"));
|
||||
given().get(school + "/dancers").then().statusCode(200).body("id", contains((int) dancerId));
|
||||
|
||||
long groupId = create(school + "/dance-groups", GROUP);
|
||||
String group = school + "/dance-groups/" + groupId;
|
||||
given().put(group + "/dancers/" + dancerId).then().statusCode(204);
|
||||
put(group, Map.of("category", "Selection", "type", "SELECTION", "startAge", 10,
|
||||
"endAge", 20, "styles", new String[]{"JAZZ", "MODERN"}))
|
||||
.statusCode(200).body("category", equalTo("Selection"))
|
||||
.body("dancerIds", contains((int) dancerId)).body("styles", containsInAnyOrder("JAZZ", "MODERN"));
|
||||
put(
|
||||
group,
|
||||
Map.of(
|
||||
"category",
|
||||
"Selection",
|
||||
"type",
|
||||
"SELECTION",
|
||||
"startAge",
|
||||
10,
|
||||
"endAge",
|
||||
20,
|
||||
"styles",
|
||||
new String[] {"JAZZ", "MODERN"}))
|
||||
.statusCode(200)
|
||||
.body("category", equalTo("Selection"))
|
||||
.body("dancerIds", contains((int) dancerId))
|
||||
.body("styles", containsInAnyOrder("JAZZ", "MODERN"));
|
||||
given().get(group).then().statusCode(200).body("type", equalTo("SELECTION"));
|
||||
given().get(school + "/dance-groups").then().statusCode(200).body("id", contains((int) groupId));
|
||||
|
||||
long showId = create(school + "/shows", SHOW);
|
||||
String show = school + "/shows/" + showId;
|
||||
given().get(show).then().statusCode(200)
|
||||
given().get(show)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("dayParts.type", contains("AFTERNOON"))
|
||||
.body("dayParts.danceGroupIds", everyItem(empty()));
|
||||
put(show, Map.of("name", "Summer show", "dayParts", new Object[]{Map.of("date", "2027-07-01", "type", "EVENING")}))
|
||||
.statusCode(200).body("dayParts.date", contains("2027-07-01"));
|
||||
put(show, Map.of("name", "Summer show", "dayParts", new Object[] {
|
||||
Map.of("date", "2027-07-01", "type", "EVENING")
|
||||
}))
|
||||
.statusCode(200)
|
||||
.body("dayParts.date", contains("2027-07-01"));
|
||||
given().get(school + "/shows").then().statusCode(200).body("id", contains((int) showId));
|
||||
given().get(school).then().statusCode(200).body("dancerIds", contains((int) dancerId))
|
||||
.body("danceGroupIds", contains((int) groupId)).body("showIds", contains((int) showId));
|
||||
given().get(school)
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("dancerIds", contains((int) dancerId))
|
||||
.body("danceGroupIds", contains((int) groupId))
|
||||
.body("showIds", contains((int) showId));
|
||||
|
||||
for (String path : new String[]{dancer, group, show}) {
|
||||
for (String path : new String[] {dancer, group, show}) {
|
||||
given().delete(path).then().statusCode(204);
|
||||
given().get(path).then().statusCode(404);
|
||||
given().delete(path).then().statusCode(404);
|
||||
}
|
||||
given().get(school).then().body("dancerIds", empty()).body("danceGroupIds", empty()).body("showIds", empty());
|
||||
given().get(school)
|
||||
.then()
|
||||
.body("dancerIds", empty())
|
||||
.body("danceGroupIds", empty())
|
||||
.body("showIds", empty());
|
||||
} finally {
|
||||
given().delete(school).then().statusCode(204);
|
||||
}
|
||||
@@ -71,7 +100,7 @@ class SchoolManagementResourceTest {
|
||||
long dancerId = create(school + "/dancers", DANCER);
|
||||
String first = school + "/dance-groups/" + create(school + "/dance-groups", GROUP);
|
||||
String second = school + "/dance-groups/" + create(school + "/dance-groups", GROUP);
|
||||
for (String group : new String[]{first, second}) {
|
||||
for (String group : new String[] {first, second}) {
|
||||
given().put(group + "/dancers/" + dancerId).then().statusCode(204);
|
||||
given().put(group + "/dancers/" + dancerId).then().statusCode(204);
|
||||
given().get(group).then().body("dancerIds", contains((int) dancerId));
|
||||
@@ -98,14 +127,16 @@ class SchoolManagementResourceTest {
|
||||
long group = create(first + "/dance-groups", GROUP);
|
||||
long show = create(first + "/shows", SHOW);
|
||||
long otherGroup = create(second + "/dance-groups", GROUP);
|
||||
for (String path : new String[]{"/dancers/" + dancer, "/dance-groups/" + group, "/shows/" + show}) {
|
||||
for (String path : new String[] {"/dancers/" + dancer, "/dance-groups/" + group, "/shows/" + show}) {
|
||||
given().get(second + path).then().statusCode(404);
|
||||
given().delete(second + path).then().statusCode(404);
|
||||
}
|
||||
put(second + "/dancers/" + dancer, DANCER).statusCode(404);
|
||||
put(second + "/dance-groups/" + group, GROUP).statusCode(404);
|
||||
put(second + "/shows/" + show, SHOW).statusCode(404);
|
||||
given().put(second + "/dance-groups/" + otherGroup + "/dancers/" + dancer).then().statusCode(404);
|
||||
given().put(second + "/dance-groups/" + otherGroup + "/dancers/" + dancer)
|
||||
.then()
|
||||
.statusCode(404);
|
||||
given().get(second + "/dance-groups/" + otherGroup).then().body("dancerIds", empty());
|
||||
given().delete(first).then().statusCode(204);
|
||||
given().get(first + "/dancers").then().statusCode(404);
|
||||
@@ -121,30 +152,66 @@ class SchoolManagementResourceTest {
|
||||
@Test
|
||||
void rejectsInvalidInputWithoutChangingStoredData() {
|
||||
given().contentType("application/json").body("{}").post(SCHOOLS).then().statusCode(400);
|
||||
given().contentType("application/json").body("null").post(SCHOOLS).then().statusCode(400);
|
||||
given().contentType("application/json")
|
||||
.body("null")
|
||||
.post(SCHOOLS)
|
||||
.then()
|
||||
.statusCode(400);
|
||||
String school = SCHOOLS + "/" + create(SCHOOLS, Map.of("name", "Validation school"));
|
||||
try {
|
||||
put(school, Map.of("name", " ")).statusCode(400);
|
||||
given().get(school).then().body("name", equalTo("Validation school"));
|
||||
for (Object body : new Object[]{Map.of("name", "Sam"), Map.of("name", "Sam", "birthDate", "2999-01-01"),
|
||||
Map.of("name", "Sam", "birthDate", "not-a-date")}) {
|
||||
given().contentType("application/json").body(body).post(school + "/dancers").then().statusCode(400);
|
||||
for (Object body : new Object[] {
|
||||
Map.of("name", "Sam"),
|
||||
Map.of("name", "Sam", "birthDate", "2999-01-01"),
|
||||
Map.of("name", "Sam", "birthDate", "not-a-date")
|
||||
}) {
|
||||
given().contentType("application/json")
|
||||
.body(body)
|
||||
.post(school + "/dancers")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
for (String body : new String[]{"{}", "{\"category\":\"Teens\",\"type\":\"INVALID\"}",
|
||||
"{\"category\":\"Teens\",\"type\":\"REGULAR\",\"startAge\":-1,\"endAge\":18,\"styles\":[]}",
|
||||
"{\"category\":\"Teens\",\"type\":\"REGULAR\",\"startAge\":18,\"endAge\":12,\"styles\":[]}"}) {
|
||||
given().contentType("application/json").body(body).post(school + "/dance-groups").then().statusCode(400);
|
||||
for (String body : new String[] {
|
||||
"{}",
|
||||
"{\"category\":\"Teens\",\"type\":\"INVALID\"}",
|
||||
"{\"category\":\"Teens\",\"type\":\"REGULAR\",\"startAge\":-1,\"endAge\":18,\"styles\":[]}",
|
||||
"{\"category\":\"Teens\",\"type\":\"REGULAR\",\"startAge\":18,\"endAge\":12,\"styles\":[]}"
|
||||
}) {
|
||||
given().contentType("application/json")
|
||||
.body(body)
|
||||
.post(school + "/dance-groups")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
}
|
||||
long group = create(school + "/dance-groups", GROUP);
|
||||
put(school + "/dance-groups/" + group, Map.of("category", "Invalid", "type", "REGULAR",
|
||||
"startAge", 20, "endAge", 10, "styles", new String[]{})).statusCode(400);
|
||||
put(
|
||||
school + "/dance-groups/" + group,
|
||||
Map.of(
|
||||
"category",
|
||||
"Invalid",
|
||||
"type",
|
||||
"REGULAR",
|
||||
"startAge",
|
||||
20,
|
||||
"endAge",
|
||||
10,
|
||||
"styles",
|
||||
new String[] {}))
|
||||
.statusCode(400);
|
||||
given().get(school + "/dance-groups/" + group).then().body("category", equalTo("Teens"));
|
||||
given().contentType("application/json").body(Map.of("name", "Show"))
|
||||
.post(school + "/shows").then().statusCode(400);
|
||||
given().contentType("application/json")
|
||||
.body(Map.of("name", "Show"))
|
||||
.post(school + "/shows")
|
||||
.then()
|
||||
.statusCode(400);
|
||||
given().get(school + "/dancers").then().body("$", empty());
|
||||
given().get(school + "/shows").then().body("$", empty());
|
||||
given().contentType("application/json").body(DANCER)
|
||||
.post(SCHOOLS + "/9223372036854775807/dancers").then().statusCode(404);
|
||||
given().contentType("application/json")
|
||||
.body(DANCER)
|
||||
.post(SCHOOLS + "/9223372036854775807/dancers")
|
||||
.then()
|
||||
.statusCode(404);
|
||||
} finally {
|
||||
given().delete(school).then().statusCode(204);
|
||||
}
|
||||
@@ -161,22 +228,44 @@ class SchoolManagementResourceTest {
|
||||
given().get(show).then().body("dayParts[0].danceGroupIds", contains((int) group));
|
||||
String before = given().get(show).asString();
|
||||
String counts = "{\"SHOW_CASE\":1,\"PRE_EDUCATION\":1,\"SELECTION\":1,\"REGULAR\":%s,\"PRE_SCHOOLER\":1}";
|
||||
for (String count : new String[]{"0", "2", "1.5", "null"}) {
|
||||
String body = "{\"name\":\"Show\",\"dayParts\":[{\"date\":\"2027-06-12\",\"type\":\"AFTERNOON\"}],\"appearances\":" + counts.formatted(count) + "}";
|
||||
for (String count : new String[] {"0", "2", "1.5", "null"}) {
|
||||
String body =
|
||||
"{\"name\":\"Show\",\"dayParts\":[{\"date\":\"2027-06-12\",\"type\":\"AFTERNOON\"}],\"appearances\":"
|
||||
+ counts.formatted(count) + "}";
|
||||
put(show, body).statusCode(400);
|
||||
given().get(show).then().body(equalTo(before));
|
||||
}
|
||||
String exported = given().get(school + "/export").then().statusCode(200).extract().asString();
|
||||
given().contentType("application/json").body(exported).post(SCHOOLS + "/import?replaceId=" + school.substring(school.lastIndexOf('/') + 1))
|
||||
.then().statusCode(200);
|
||||
given().get(school + "/export").then().body("$", equalTo(io.restassured.path.json.JsonPath.from(exported).getMap("$")));
|
||||
} finally { given().delete(school); }
|
||||
String exported = given().get(school + "/export")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.extract()
|
||||
.asString();
|
||||
given().contentType("application/json")
|
||||
.body(exported)
|
||||
.post(SCHOOLS + "/import?replaceId=" + school.substring(school.lastIndexOf('/') + 1))
|
||||
.then()
|
||||
.statusCode(200);
|
||||
given().get(school + "/export")
|
||||
.then()
|
||||
.body(
|
||||
"$",
|
||||
equalTo(io.restassured.path.json.JsonPath.from(exported)
|
||||
.getMap("$")));
|
||||
} finally {
|
||||
given().delete(school);
|
||||
}
|
||||
}
|
||||
|
||||
private static long create(String path, Object body) {
|
||||
return given().contentType("application/json").body(body).post(path).then()
|
||||
.statusCode(201).header("Location", matchesPattern(".*/[0-9]+$"))
|
||||
.extract().jsonPath().getLong("id");
|
||||
return given().contentType("application/json")
|
||||
.body(body)
|
||||
.post(path)
|
||||
.then()
|
||||
.statusCode(201)
|
||||
.header("Location", matchesPattern(".*/[0-9]+$"))
|
||||
.extract()
|
||||
.jsonPath()
|
||||
.getLong("id");
|
||||
}
|
||||
|
||||
private static ValidatableResponse put(String path, Object body) {
|
||||
|
||||
@@ -1,32 +1,68 @@
|
||||
package nl.agilecc.agiledancers;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
@QuarkusTest
|
||||
class SchoolSnapshotResourceTest {
|
||||
private static final String BASE = "/api/dance-schools";
|
||||
@Test void downloadPreviewRestoreAndRejectInvalidFiles() {
|
||||
long id = given().contentType("application/json").body("{\"name\":\"JSON school\"}").post(BASE)
|
||||
.then().statusCode(201).extract().jsonPath().getLong("id");
|
||||
String json = given().get(BASE + "/" + id + "/export").then().statusCode(200)
|
||||
.header("Content-Disposition", containsString(".json")).body("formatVersion", equalTo(2)).extract().asString();
|
||||
|
||||
@Test
|
||||
void downloadPreviewRestoreAndRejectInvalidFiles() {
|
||||
long id = given().contentType("application/json")
|
||||
.body("{\"name\":\"JSON school\"}")
|
||||
.post(BASE)
|
||||
.then()
|
||||
.statusCode(201)
|
||||
.extract()
|
||||
.jsonPath()
|
||||
.getLong("id");
|
||||
String json = given().get(BASE + "/" + id + "/export")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.header("Content-Disposition", containsString(".json"))
|
||||
.body("formatVersion", equalTo(2))
|
||||
.extract()
|
||||
.asString();
|
||||
try {
|
||||
given().contentType("application/json").body(json).post(BASE + "/import/preview").then().statusCode(200)
|
||||
.body("existingId", equalTo((int) id)).body("dancers", equalTo(0));
|
||||
for (String bad : new String[]{"{", "null", json.replace("\"formatVersion\" : 2", "\"formatVersion\" : 1"),
|
||||
json.replace("\"name\" :", "\"unknown\" :"), json + " {}",
|
||||
json.replace("\"dancers\" : [ ]", "\"dancers\" : null")}) {
|
||||
given().contentType("application/json").body(bad).post(BASE + "/import?replaceId=" + id)
|
||||
.then().statusCode(400).body("message", not(emptyOrNullString()));
|
||||
given().contentType("application/json")
|
||||
.body(json)
|
||||
.post(BASE + "/import/preview")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.body("existingId", equalTo((int) id))
|
||||
.body("dancers", equalTo(0));
|
||||
for (String bad : new String[] {
|
||||
"{",
|
||||
"null",
|
||||
json.replace("\"formatVersion\" : 2", "\"formatVersion\" : 1"),
|
||||
json.replace("\"name\" :", "\"unknown\" :"),
|
||||
json + " {}",
|
||||
json.replace("\"dancers\" : [ ]", "\"dancers\" : null")
|
||||
}) {
|
||||
given().contentType("application/json")
|
||||
.body(bad)
|
||||
.post(BASE + "/import?replaceId=" + id)
|
||||
.then()
|
||||
.statusCode(400)
|
||||
.body("message", not(emptyOrNullString()));
|
||||
}
|
||||
given().get(BASE + "/" + id + "/export").then().body(equalTo(json));
|
||||
given().delete(BASE + "/" + id).then().statusCode(204);
|
||||
id = given().contentType("application/json").body(json).post(BASE + "/import")
|
||||
.then().statusCode(200).extract().jsonPath().getLong("id");
|
||||
id = given().contentType("application/json")
|
||||
.body(json)
|
||||
.post(BASE + "/import")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.extract()
|
||||
.jsonPath()
|
||||
.getLong("id");
|
||||
given().get(BASE + "/" + id + "/export").then().body(equalTo(json));
|
||||
} finally { given().delete(BASE + "/" + id); }
|
||||
} finally {
|
||||
given().delete(BASE + "/" + id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,51 @@
|
||||
package nl.agilecc.agiledancers;
|
||||
|
||||
import nl.agilecc.agiledancers.service.model.*;
|
||||
import nl.agilecc.agiledancers.service.*;
|
||||
import nl.agilecc.agiledancers.model.specifications.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import nl.agilecc.agiledancers.model.specifications.*;
|
||||
import nl.agilecc.agiledancers.service.*;
|
||||
import nl.agilecc.agiledancers.service.model.*;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchoolSnapshotTest {
|
||||
private SchoolSnapshot complete() {
|
||||
return new SchoolSnapshot(2, UUID.randomUUID(), "Studio", List.of(new Person("a", "Sam", LocalDate.of(2010,1,1))),
|
||||
return new SchoolSnapshot(
|
||||
2,
|
||||
UUID.randomUUID(),
|
||||
"Studio",
|
||||
List.of(new Person("a", "Sam", LocalDate.of(2010, 1, 1))),
|
||||
List.of(new Trainer("c", "Alex")),
|
||||
List.of(new Group("g", "Hiphop", DanceGroupType.REGULAR, 0, 99, List.of(DanceGroupStyle.HIPHOP), List.of("a"), "c"),
|
||||
List.of(
|
||||
new Group(
|
||||
"g",
|
||||
"Hiphop",
|
||||
DanceGroupType.REGULAR,
|
||||
0,
|
||||
99,
|
||||
List.of(DanceGroupStyle.HIPHOP),
|
||||
List.of("a"),
|
||||
"c"),
|
||||
new Group("h", "Jazz", DanceGroupType.REGULAR, 0, 99, List.of(), List.of("a"), "c")),
|
||||
List.of(new Performance("s", "Show",
|
||||
List.of(new Part(LocalDate.of(2027,1,1), DayPartType.MORNING, List.of("g", "h"))), ShowConfiguration.defaults(1))));
|
||||
List.of(new Performance(
|
||||
"s",
|
||||
"Show",
|
||||
List.of(new Part(LocalDate.of(2027, 1, 1), DayPartType.MORNING, List.of("g", "h"))),
|
||||
ShowConfiguration.defaults(1))));
|
||||
}
|
||||
@Test void roundTripAfterRestartPreservesAllFieldsAndSharedRelationships() {
|
||||
|
||||
@Test
|
||||
void roundTripAfterRestartPreservesAllFieldsAndSharedRelationships() {
|
||||
var snapshot = complete();
|
||||
var service = new SchoolManagementService();
|
||||
var existing = service.createSchool("Other");
|
||||
service.createDancer(existing.getId(), "Other dancer", LocalDate.of(2000,1,1));
|
||||
service.createDancer(existing.getId(), "Other dancer", LocalDate.of(2000, 1, 1));
|
||||
var imported = service.importSchool(snapshot, null);
|
||||
assertEquals(2, imported.getDanceGroups().size());
|
||||
var dancer = imported.getDancers().iterator().next();
|
||||
imported.getDanceGroups().forEach(g -> assertSame(dancer, g.getDancers().iterator().next()));
|
||||
imported.getDanceGroups()
|
||||
.forEach(g -> assertSame(dancer, g.getDancers().iterator().next()));
|
||||
assertEquals(SchoolSnapshot.of(snapshot.restore()), service.exportSchool(imported.getId()));
|
||||
var restarted = new SchoolManagementService();
|
||||
var restored = restarted.importSchool(service.exportSchool(imported.getId()), null);
|
||||
@@ -36,14 +56,26 @@ class SchoolSnapshotTest {
|
||||
assertEquals(1, existing.getDancers().size());
|
||||
var group = imported.getDanceGroups().iterator().next();
|
||||
service.deleteGroup(imported.getId(), group.getId());
|
||||
assertEquals(1, imported.getShows().iterator().next().getDayParts().getFirst().getDanceGroups().size());
|
||||
assertEquals(
|
||||
1,
|
||||
imported.getShows()
|
||||
.iterator()
|
||||
.next()
|
||||
.getDayParts()
|
||||
.getFirst()
|
||||
.getDanceGroups()
|
||||
.size());
|
||||
assertDoesNotThrow(() -> service.exportSchool(imported.getId()).restore());
|
||||
}
|
||||
@Test void previewIsReadOnlyAndReplacementRemovesPreviousConfiguration() {
|
||||
var service = new SchoolManagementService(); var snapshot = complete();
|
||||
assertNull(service.preview(snapshot).existingId()); assertTrue(service.schools().isEmpty());
|
||||
|
||||
@Test
|
||||
void previewIsReadOnlyAndReplacementRemovesPreviousConfiguration() {
|
||||
var service = new SchoolManagementService();
|
||||
var snapshot = complete();
|
||||
assertNull(service.preview(snapshot).existingId());
|
||||
assertTrue(service.schools().isEmpty());
|
||||
var original = service.importSchool(snapshot, null);
|
||||
service.createDancer(original.getId(), "Extra", LocalDate.of(2000,1,1));
|
||||
service.createDancer(original.getId(), "Extra", LocalDate.of(2000, 1, 1));
|
||||
var other = service.createSchool(snapshot.name());
|
||||
assertEquals(original.getId(), service.preview(snapshot).existingId());
|
||||
assertThrows(ManagementException.class, () -> service.importSchool(snapshot, null));
|
||||
@@ -52,20 +84,31 @@ class SchoolSnapshotTest {
|
||||
assertEquals(SchoolSnapshot.of(snapshot.restore()), service.exportSchool(replaced.getId()));
|
||||
assertSame(other, service.school(other.getId()));
|
||||
service.deleteSchool(replaced.getId());
|
||||
assertEquals(SchoolSnapshot.of(snapshot.restore()), service.exportSchool(service.importSchool(snapshot, null).getId()));
|
||||
assertEquals(
|
||||
SchoolSnapshot.of(snapshot.restore()),
|
||||
service.exportSchool(service.importSchool(snapshot, null).getId()));
|
||||
}
|
||||
@Test void unresolvedReferenceLeavesExistingAndOtherSchoolsIntact() {
|
||||
var service = new SchoolManagementService(); var valid = complete();
|
||||
|
||||
@Test
|
||||
void unresolvedReferenceLeavesExistingAndOtherSchoolsIntact() {
|
||||
var service = new SchoolManagementService();
|
||||
var valid = complete();
|
||||
var original = service.importSchool(valid, null);
|
||||
var bad = new SchoolSnapshot(2, valid.key(), valid.name(), List.of(), valid.coaches(), valid.groups(), valid.shows());
|
||||
var bad = new SchoolSnapshot(
|
||||
2, valid.key(), valid.name(), List.of(), valid.coaches(), valid.groups(), valid.shows());
|
||||
assertThrows(ManagementException.class, () -> service.preview(bad));
|
||||
assertThrows(ManagementException.class, () -> service.importSchool(bad, original.getId()));
|
||||
assertEquals(SchoolSnapshot.of(valid.restore()), service.exportSchool(original.getId()));
|
||||
}
|
||||
@Test void emptySchoolRoundTrips() {
|
||||
|
||||
@Test
|
||||
void emptySchoolRoundTrips() {
|
||||
var service = new SchoolManagementService();
|
||||
var empty = service.createSchool("Empty"); var snapshot = service.exportSchool(empty.getId());
|
||||
var empty = service.createSchool("Empty");
|
||||
var snapshot = service.exportSchool(empty.getId());
|
||||
service.deleteSchool(empty.getId());
|
||||
assertEquals(SchoolSnapshot.of(snapshot.restore()), service.exportSchool(service.importSchool(snapshot, null).getId()));
|
||||
assertEquals(
|
||||
SchoolSnapshot.of(snapshot.restore()),
|
||||
service.exportSchool(service.importSchool(snapshot, null).getId()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,44 @@
|
||||
package nl.agilecc.agiledancers.service;
|
||||
|
||||
import nl.agilecc.agiledancers.service.port.SchedulingEngine;
|
||||
import nl.agilecc.agiledancers.model.*;
|
||||
import nl.agilecc.agiledancers.model.specifications.*;
|
||||
import nl.agilecc.agiledancers.solver.OrToolsSchedulingEngine;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import nl.agilecc.agiledancers.model.*;
|
||||
import nl.agilecc.agiledancers.model.specifications.*;
|
||||
import nl.agilecc.agiledancers.service.port.SchedulingEngine;
|
||||
import nl.agilecc.agiledancers.solver.OrToolsSchedulingEngine;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SchedulingServiceTest {
|
||||
@Test void failedAndNoResultRunsPreserveAssignmentsAndReleaseTheRunGuard() {
|
||||
@Test
|
||||
void failedAndNoResultRunsPreserveAssignmentsAndReleaseTheRunGuard() {
|
||||
var schools = new SchoolManagementService();
|
||||
long school = schools.createSchool("School").getId();
|
||||
schools.createGroup(school, "Group", DanceGroupType.REGULAR, 0, 99, Set.of());
|
||||
var show = schools.createShow(school, "Show", List.of(DayPart.builder().date(LocalDate.of(2027,1,1))
|
||||
.type(DayPartType.MORNING).danceGroups(List.of()).build()), null);
|
||||
var show = schools.createShow(
|
||||
school,
|
||||
"Show",
|
||||
List.of(DayPart.builder()
|
||||
.date(LocalDate.of(2027, 1, 1))
|
||||
.type(DayPartType.MORNING)
|
||||
.danceGroups(List.of())
|
||||
.build()),
|
||||
null);
|
||||
var scheduling = new SchedulingService();
|
||||
scheduling.schools = schools;
|
||||
scheduling.engine = new OrToolsSchedulingEngine();
|
||||
scheduling.schedule(school, show.getId());
|
||||
var before = schools.exportSchool(school);
|
||||
for (SchedulingEngine failure : List.<SchedulingEngine>of(
|
||||
input -> { throw new IllegalStateException("Native failure"); },
|
||||
input -> { throw new ManagementException(ManagementExceptionKind.INVALID_INPUT, "No valid result before the limit"); },
|
||||
input -> {
|
||||
throw new IllegalStateException("Native failure");
|
||||
},
|
||||
input -> {
|
||||
throw new ManagementException(
|
||||
ManagementExceptionKind.INVALID_INPUT, "No valid result before the limit");
|
||||
},
|
||||
input -> null)) {
|
||||
scheduling.engine = failure;
|
||||
assertThrows(ManagementException.class, () -> scheduling.schedule(school, show.getId()));
|
||||
@@ -34,28 +48,46 @@ class SchedulingServiceTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test void concurrentRunIsRejectedWhileEditsRemainPossibleAndInvalidateTheFirstResult() throws Exception {
|
||||
@Test
|
||||
void concurrentRunIsRejectedWhileEditsRemainPossibleAndInvalidateTheFirstResult() throws Exception {
|
||||
var schools = new SchoolManagementService();
|
||||
long school = schools.createSchool("School").getId();
|
||||
schools.createGroup(school, "Group", DanceGroupType.REGULAR, 0, 99, Set.of());
|
||||
var show = schools.createShow(school, "Show", List.of(DayPart.builder().date(LocalDate.of(2027,1,1))
|
||||
.type(DayPartType.MORNING).danceGroups(List.of()).build()), null);
|
||||
var scheduling = new SchedulingService(); scheduling.schools = schools;
|
||||
var entered = new CountDownLatch(1); var release = new CountDownLatch(1);
|
||||
var show = schools.createShow(
|
||||
school,
|
||||
"Show",
|
||||
List.of(DayPart.builder()
|
||||
.date(LocalDate.of(2027, 1, 1))
|
||||
.type(DayPartType.MORNING)
|
||||
.danceGroups(List.of())
|
||||
.build()),
|
||||
null);
|
||||
var scheduling = new SchedulingService();
|
||||
scheduling.schools = schools;
|
||||
var entered = new CountDownLatch(1);
|
||||
var release = new CountDownLatch(1);
|
||||
scheduling.engine = input -> {
|
||||
entered.countDown();
|
||||
try { if (!release.await(10, TimeUnit.SECONDS)) throw new IllegalStateException("Test timeout"); }
|
||||
catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new IllegalStateException(e); }
|
||||
try {
|
||||
if (!release.await(10, TimeUnit.SECONDS)) throw new IllegalStateException("Test timeout");
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return new OrToolsSchedulingEngine().solve(input);
|
||||
};
|
||||
try (var executor = Executors.newSingleThreadExecutor()) {
|
||||
var first = executor.submit(() -> scheduling.schedule(school, show.getId()));
|
||||
try {
|
||||
assertTrue(entered.await(10, TimeUnit.SECONDS));
|
||||
assertEquals(ManagementExceptionKind.CONFLICT, assertThrows(ManagementException.class,
|
||||
() -> scheduling.schedule(school, show.getId())).getKind());
|
||||
assertEquals(
|
||||
ManagementExceptionKind.CONFLICT,
|
||||
assertThrows(ManagementException.class, () -> scheduling.schedule(school, show.getId()))
|
||||
.getKind());
|
||||
schools.createGroup(school, "New group", DanceGroupType.REGULAR, 0, 99, Set.of());
|
||||
} finally { release.countDown(); }
|
||||
} finally {
|
||||
release.countDown();
|
||||
}
|
||||
var exception = assertThrows(ExecutionException.class, () -> first.get(10, TimeUnit.SECONDS));
|
||||
assertInstanceOf(ManagementException.class, exception.getCause());
|
||||
assertTrue(exception.getCause().getMessage().contains("inputs changed"));
|
||||
|
||||
Reference in New Issue
Block a user