// Copyright © 2016 HSL <https://www.hsl.fi> // This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses. package fi.hsl.parkandride.itest; import com.google.common.collect.ImmutableMap; import com.jayway.restassured.path.json.JsonPath; import com.jayway.restassured.response.Response; import fi.hsl.parkandride.back.Dummies; import fi.hsl.parkandride.core.back.PredictionRepository; import fi.hsl.parkandride.core.domain.*; import fi.hsl.parkandride.core.domain.prediction.HubPredictionResult; import fi.hsl.parkandride.core.domain.prediction.PredictionResult; import fi.hsl.parkandride.core.service.FacilityService; import fi.hsl.parkandride.core.service.PredictionService; import fi.hsl.parkandride.front.UrlSchema; import org.joda.time.DateTime; import org.junit.Before; import org.junit.Test; import org.springframework.http.HttpStatus; import javax.inject.Inject; import java.time.Duration; import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.Collections; import java.util.Map; import java.util.Set; import static com.jayway.restassured.RestAssured.when; import static fi.hsl.parkandride.core.domain.CapacityType.CAR; import static fi.hsl.parkandride.core.domain.CapacityType.ELECTRIC_CAR; import static fi.hsl.parkandride.core.domain.DayType.*; import static fi.hsl.parkandride.core.domain.PricingMethod.CUSTOM; import static fi.hsl.parkandride.core.domain.PricingMethod.PARK_AND_RIDE_247_FREE; import static fi.hsl.parkandride.core.domain.Role.ADMIN; import static fi.hsl.parkandride.core.domain.Role.OPERATOR_API; import static fi.hsl.parkandride.core.domain.Usage.*; import static java.util.Arrays.asList; import static java.util.Arrays.stream; import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.groups.Tuple.tuple; import static org.hamcrest.Matchers.containsString; public class PredictionITest extends AbstractIntegrationTest { private static final int SPACES_AVAILABLE = 42; @Inject Dummies dummies; @Inject FacilityService facilityService; @Inject PredictionService predictionService; private long facilityId; private Facility f; private User user; private final DateTime now = new DateTime(); private User adminUser; @Before public void initFixture() { devHelper.deleteAll(); facilityId = dummies.createFacility(); f = facilityService.getFacility(facilityId); Long operatorId = f.operatorId; user = devHelper.createOrUpdateUser(new NewUser(1L, "operator", OPERATOR_API, operatorId, "operator")); adminUser = devHelper.createOrUpdateUser(new NewUser(100L, "admin", ADMIN, "admin")); // Ensure validation passes f.pricingMethod = PricingMethod.PARK_AND_RIDE_247_FREE; f.pricing = emptyList(); facilityService.updateFacility(f.id, f, adminUser); } @Test public void prediction_JSON_structure() { Utilization u = makeDummyPredictions(); JsonPath json = when().get(UrlSchema.FACILITY_PREDICTION, facilityId).jsonPath(); long facilityId = json.getLong("[0].facilityId"); String capacityType = json.getString("[0].capacityType"); String usage = json.getString("[0].usage"); OffsetDateTime timestamp = OffsetDateTime.parse(json.getString("[0].timestamp"), DateTimeFormatter.ISO_OFFSET_DATE_TIME); int spacesAvailable = json.getInt("[0].spacesAvailable"); assertThat(facilityId).as("facilityId").isEqualTo(u.facilityId); assertThat(capacityType).as("capacityType").isEqualTo(u.capacityType.name()); assertThat(usage).as("usage").isEqualTo(u.usage.name()); assertThat(timestamp.getOffset()).as("time should be in local timezone") .isEqualTo(ZoneOffset.systemDefault().getRules().getOffset(timestamp.toInstant())); assertThat(spacesAvailable).as("spacesAvailable").isEqualTo(u.spacesAvailable); } @Test public void prediction_API_is_symmetric_with_utilization_API() { makeDummyPredictions(); Map<String, Object> utilization = when().get(UrlSchema.FACILITY_UTILIZATION, facilityId).jsonPath().getMap("[0]"); Map<String, Object> prediction = when().get(UrlSchema.FACILITY_PREDICTION, facilityId).jsonPath().getMap("[0]"); assertThat(utilization).as("utilization").isNotEmpty(); assertThat(prediction).as("prediction").isNotEmpty(); Set<String> expectedFields = utilization.keySet(); expectedFields.remove("capacity"); // TODO: we are not predicting the capacity; should we add the capacity field to predictions? assertThat(prediction.keySet()).as("prediction's fields should be a superset of utilization's fields") .containsAll(expectedFields); } @Test public void returns_predictions_for_all_capacity_types_and_usages() { makeDummyPredictions(Usage.HSL_TRAVEL_CARD); makeDummyPredictions(Usage.COMMERCIAL); f.builtCapacity = ImmutableMap.of(CAR, 1000); f.pricingMethod = CUSTOM; f.pricing = asList( new Pricing(CAR, HSL_TRAVEL_CARD, 1000, BUSINESS_DAY, "00", "24", "0"), new Pricing(CAR, HSL_TRAVEL_CARD, 1000, SATURDAY, "00", "24", "0"), new Pricing(CAR, HSL_TRAVEL_CARD, 1000, SUNDAY, "00", "24", "0"), new Pricing(CAR, COMMERCIAL, 1000, BUSINESS_DAY, "00", "24", "0"), new Pricing(CAR, COMMERCIAL, 1000, SATURDAY, "00", "24", "0"), new Pricing(CAR, COMMERCIAL, 1000, SUNDAY, "00", "24", "0") ); facilityService.updateFacility(f.id, f, adminUser); PredictionResult[] predictions = getPredictions(facilityId); assertThat(predictions).hasSize(2); } @Test public void defaults_to_prediction_for_current_time() { makeDummyPredictions(); PredictionResult[] predictions = getPredictions(facilityId); assertThat(predictions).hasSize(1); assertIsNear(DateTime.now(), predictions[0].timestamp); } @Test public void can_find_predictions_by_absolute_time() { makeDummyPredictions(); DateTime requestedTime = now.plusHours(5); PredictionResult[] predictions = getPredictionsAtAbsoluteTime(facilityId, requestedTime); assertThat(predictions).hasSize(1); assertIsNear(requestedTime, predictions[0].timestamp); } @Test public void timezone_is_required_for_absolute_time() { makeDummyPredictions(); DateTime requestedTime = now.plusHours(5); // TODO: make it return the error message "timezone is required" or similar when().get(UrlSchema.FACILITY_PREDICTION_ABSOLUTE, facilityId, requestedTime.toLocalDateTime()) .then().assertThat().statusCode(HttpStatus.BAD_REQUEST.value()) .assertThat().content(containsString("Invalid format")); } @Test public void can_find_predictions_by_relative_time() { makeDummyPredictions(); DateTime requestedTime = now.plusHours(5).plusMinutes(30); PredictionResult[] predictions = getPredictionsAfterRelativeTime(facilityId, "5:30"); assertThat(predictions).hasSize(1); assertIsNear(requestedTime, predictions[0].timestamp); } @Test public void relative_time_may_be_expressed_in_minutes() { // i.e. hours are optional and minutes can be over 60 makeDummyPredictions(); DateTime requestedTime = now.plusMinutes(180); PredictionResult[] predictions = getPredictionsAfterRelativeTime(facilityId, "180"); assertThat(predictions).hasSize(1); assertIsNear(requestedTime, predictions[0].timestamp); } @Test public void does_not_show_prediction_if_no_built_capacity() { makeDummyPredictions(); assertThat(getPredictions(facilityId)).hasSize(1) .extracting(pr -> pr.capacityType) .containsExactly(CapacityType.CAR); f.builtCapacity = ImmutableMap.of(CapacityType.BICYCLE, 1); facilityService.updateFacility(f.id, f, adminUser); assertThat(getPredictions(facilityId)).isEmpty(); } @Test public void does_not_show_prediction_if_no_usage() { makeDummyPredictions(COMMERCIAL); assertThat(getPredictions(facilityId)).isEmpty(); } @Test public void does_not_show_prediction_if_usage_for_different_capacity_type() { // CAR & COMMERCIAL should not show up in the list // ELECTRIC_CAR & PARK_AND_RIDE should not show up in the list makeDummyPredictions(CAR, PARK_AND_RIDE); makeDummyPredictions(ELECTRIC_CAR, PARK_AND_RIDE); makeDummyPredictions(CAR, COMMERCIAL); makeDummyPredictions(ELECTRIC_CAR, COMMERCIAL); f.builtCapacity = ImmutableMap.of(CAR, 100, ELECTRIC_CAR, 50); f.pricingMethod = CUSTOM; f.pricing = asList( new Pricing(CAR, PARK_AND_RIDE, 100, BUSINESS_DAY, "00", "24", "0"), new Pricing(CAR, PARK_AND_RIDE, 100, SATURDAY, "00", "24", "0"), new Pricing(CAR, PARK_AND_RIDE, 100, SUNDAY, "00", "24", "0"), new Pricing(ELECTRIC_CAR, COMMERCIAL, 50, BUSINESS_DAY, "00", "24", "0"), new Pricing(ELECTRIC_CAR, COMMERCIAL, 50, SATURDAY, "00", "24", "0"), new Pricing(ELECTRIC_CAR, COMMERCIAL, 50, SUNDAY, "00", "24", "0") ); facilityService.updateFacility(f.id, f, adminUser); assertThat(getPredictions(f.id)) .extracting(pr -> tuple(pr.capacityType, pr.usage)) .hasSize(2) .containsOnly( tuple(CAR, PARK_AND_RIDE), tuple(ELECTRIC_CAR, COMMERCIAL) ); } // predictions for hubs @Test public void hub_sums_predictions_for_facilities() { final long facility2Id = dummies.createFacility(); final Long operator2Id = facilityService.getFacility(facility2Id).operatorId; final User user2 = devHelper.createOrUpdateUser(new NewUser(2L, "operator2", OPERATOR_API, operator2Id, "operator")); final long hubId = dummies.createHub(facilityId, facility2Id); makeDummyPredictions(Usage.PARK_AND_RIDE, facilityId, user, CapacityType.CAR); makeDummyPredictions(Usage.PARK_AND_RIDE, facility2Id, user2, CapacityType.CAR); makeDummyPredictions(Usage.COMMERCIAL, facility2Id, user2, CapacityType.CAR); final Facility f2 = facilityService.getFacility(facility2Id); f2.builtCapacity = ImmutableMap.of(CAR, 1000); f2.pricing = asList( new Pricing(CAR, PARK_AND_RIDE, 1000, BUSINESS_DAY, "00", "24", "0"), new Pricing(CAR, PARK_AND_RIDE, 1000, SATURDAY, "00", "24", "0"), new Pricing(CAR, PARK_AND_RIDE, 1000, SUNDAY, "00", "24", "0"), new Pricing(CAR, COMMERCIAL, 1000, BUSINESS_DAY, "00", "24", "0"), new Pricing(CAR, COMMERCIAL, 1000, SATURDAY, "00", "24", "0"), new Pricing(CAR, COMMERCIAL, 1000, SUNDAY, "00", "24", "0") );; f2.pricingMethod = PricingMethod.CUSTOM; facilityService.updateFacility(f2.id, f2, adminUser); final HubPredictionResult[] predictionsForHub = getPredictionsForHub(hubId); assertThat(predictionsForHub).hasSize(2); assertThat(predictionsForHub[0].hubId).isEqualTo(hubId); assertThat(predictionsForHub[0].capacityType).isEqualTo(CapacityType.CAR); assertIsNear(DateTime.now(), predictionsForHub[0].timestamp); // The predictions should be sums of facilities' predictions final HubPredictionResult parkAndRide = stream(predictionsForHub) .filter(pred -> pred.usage == Usage.PARK_AND_RIDE) .findFirst().get(); assertThat(parkAndRide.spacesAvailable).isEqualTo(SPACES_AVAILABLE * 2); final HubPredictionResult commercial = stream(predictionsForHub) .filter(pred -> pred.usage == Usage.COMMERCIAL) .findFirst().get(); assertThat(commercial.spacesAvailable).isEqualTo(SPACES_AVAILABLE); } @Test public void hub_excludes_prediction_if_no_built_capacity() { final long facility2Id = dummies.createFacility(); final Long operator2Id = facilityService.getFacility(facility2Id).operatorId; final User user2 = devHelper.createOrUpdateUser(new NewUser(2L, "operator2", OPERATOR_API, operator2Id, "operator")); final long hubId = dummies.createHub(facilityId, facility2Id); makeDummyPredictions(Usage.PARK_AND_RIDE, facilityId, user, CapacityType.CAR); makeDummyPredictions(Usage.PARK_AND_RIDE, facility2Id, user2, CapacityType.CAR); makeDummyPredictions(Usage.COMMERCIAL, facility2Id, user2, CapacityType.CAR); final Facility f2 = facilityService.getFacility(facility2Id); f2.pricing = emptyList(); f2.pricingMethod = PricingMethod.PARK_AND_RIDE_247_FREE; f2.builtCapacity = ImmutableMap.of(CapacityType.BICYCLE, 1); facilityService.updateFacility(f2.id, f2, adminUser); final HubPredictionResult[] predictionsForHub = getPredictionsForHub(hubId); assertThat(predictionsForHub).hasSize(1); assertThat(predictionsForHub[0].hubId).isEqualTo(hubId); assertThat(predictionsForHub[0].capacityType).isEqualTo(CapacityType.CAR); assertIsNear(DateTime.now(), predictionsForHub[0].timestamp); // Only first facility is taken into account, since the second one does not have built capacity final HubPredictionResult parkAndRide = stream(predictionsForHub) .filter(pred -> pred.usage == Usage.PARK_AND_RIDE) .findFirst().get(); assertThat(parkAndRide.spacesAvailable).isEqualTo(SPACES_AVAILABLE); } @Test public void hub_excludes_prediction_if_no_usage() { final long facility2Id = dummies.createFacility(); final Long operator2Id = facilityService.getFacility(facility2Id).operatorId; final User user2 = devHelper.createOrUpdateUser(new NewUser(2L, "operator2", OPERATOR_API, operator2Id, "operator")); final long hubId = dummies.createHub(facilityId, facility2Id); makeDummyPredictions(Usage.PARK_AND_RIDE, facilityId, user, CapacityType.CAR); makeDummyPredictions(Usage.PARK_AND_RIDE, facility2Id, user2, CapacityType.CAR); makeDummyPredictions(Usage.COMMERCIAL, facility2Id, user2, CapacityType.CAR); final Facility f2 = facilityService.getFacility(facility2Id); f2.builtCapacity = ImmutableMap.of(CAR, 1000); f2.pricingMethod = PARK_AND_RIDE_247_FREE; f2.pricing = emptyList(); facilityService.updateFacility(f2.id, f2, adminUser); final HubPredictionResult[] predictionsForHub = getPredictionsForHub(hubId); assertThat(predictionsForHub[0].hubId).isEqualTo(hubId); assertThat(predictionsForHub[0].capacityType).isEqualTo(CapacityType.CAR); assertIsNear(DateTime.now(), predictionsForHub[0].timestamp); // Commer assertThat(predictionsForHub).hasSize(1) .extracting(hpr -> hpr.usage).containsExactly(PARK_AND_RIDE); final HubPredictionResult parkAndRide = predictionsForHub[0]; assertThat(parkAndRide.spacesAvailable).isEqualTo(SPACES_AVAILABLE * 2); } @Test public void hub_with_no_facilities_returns_empty_prediction() { final long hubId = dummies.createHub(); assertThat(getPredictionsForHub(hubId)).isEmpty(); } @Test public void hub_with_no_predictions_returns_empty_prediction() { final long facility2Id = dummies.createFacility(); final long hubId = dummies.createHub(facilityId, facility2Id); assertThat(getPredictionsForHub(hubId)).isEmpty(); } // helpers private static void assertIsNear(DateTime expected, DateTime actual) { Instant i1 = Instant.ofEpochMilli(expected.getMillis()); Instant i2 = Instant.ofEpochMilli(actual.getMillis()); Duration d = Duration.between(i1, i2).abs(); assertThat(d.toMinutes()).as("distance to " + expected + " in minutes") .isLessThanOrEqualTo(PredictionRepository.PREDICTION_RESOLUTION.getMinutes()); } private static PredictionResult[] getPredictions(long facilityId) { return toPredictions(when().get(UrlSchema.FACILITY_PREDICTION, facilityId)); } private static HubPredictionResult[] getPredictionsForHub(long hubId) { return toHubPredictions(when().get(UrlSchema.HUB_PREDICTION, hubId)); } private static PredictionResult[] getPredictionsAtAbsoluteTime(long facilityId, DateTime time) { return toPredictions(when().get(UrlSchema.FACILITY_PREDICTION_ABSOLUTE, facilityId, time)); } private static PredictionResult[] getPredictionsAfterRelativeTime(long facilityId, String hhmm) { return toPredictions(when().get(UrlSchema.FACILITY_PREDICTION_RELATIVE, facilityId, hhmm)); } private static PredictionResult[] toPredictions(Response response) { return response.then().assertThat().statusCode(HttpStatus.OK.value()) .extract().as(PredictionResult[].class); } private static HubPredictionResult[] toHubPredictions(Response response) { return response.then().assertThat().statusCode(HttpStatus.OK.value()) .extract().as(HubPredictionResult[].class); } private Utilization makeDummyPredictions() { return makeDummyPredictions(Usage.PARK_AND_RIDE); } private Utilization makeDummyPredictions(Usage usage) { return makeDummyPredictions(CapacityType.CAR, usage); } private Utilization makeDummyPredictions(CapacityType capacityType, Usage usage) { return makeDummyPredictions(usage, facilityId, user, capacityType); } private Utilization makeDummyPredictions(Usage usage, long facilityId, User user, CapacityType capacityType) { Utilization u = new Utilization(); u.facilityId = facilityId; u.capacityType = capacityType; u.usage = usage; u.timestamp = now; u.spacesAvailable = SPACES_AVAILABLE; facilityService.registerUtilization(facilityId, Collections.singletonList(u), user); predictionService.updatePredictions(); return u; } }