// Copyright © 2016 HSL <https://www.hsl.fi> // This program is dual-licensed under the EUPL v1.2 and AGPLv3 licenses. package fi.hsl.parkandride.docs; import com.fasterxml.jackson.databind.ObjectMapper; import fi.hsl.parkandride.back.Dummies; import fi.hsl.parkandride.back.FacilityDaoTest; import fi.hsl.parkandride.core.back.FacilityRepository; import fi.hsl.parkandride.core.domain.*; import fi.hsl.parkandride.core.service.FacilityService; import fi.hsl.parkandride.core.service.PredictionService; import fi.hsl.parkandride.front.UrlSchema; import fi.hsl.parkandride.itest.AbstractIntegrationTest; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.springframework.boot.test.IntegrationTest; import org.springframework.http.MediaType; import org.springframework.restdocs.JUnitRestDocumentation; import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler; import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor; import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import javax.inject.Inject; import java.util.Arrays; import java.util.Collections; import java.util.List; import static com.fasterxml.jackson.databind.node.JsonNodeType.NUMBER; import static fi.hsl.parkandride.core.domain.Role.OPERATOR_API; import static fi.hsl.parkandride.docs.UriHostReplacingOperationPreprocessor.replaceUriHost; import static org.hamcrest.Matchers.*; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.operation.preprocess.Preprocessors.*; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @IntegrationTest({ "server.port:0", "spring.jackson.serialization.indent_output:true"}) public class ApiDocumentation extends AbstractIntegrationTest { @Rule public final JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation("target/generated-snippets"); @Inject Dummies dummies; @Inject FacilityRepository facilityRepository; @Inject FacilityService facilityService; @Inject PredictionService predictionService; @Inject WebApplicationContext context; @Inject ObjectMapper objectMapper; private OperationRequestPreprocessor requestPreprocessor; private OperationResponsePreprocessor responsePreprocessor; private RestDocumentationResultHandler documentationHandler; private MockMvc mockMvc; private DateTimeZone originalDateTimeZone; private String authToken; private long facilityId; private NewUser currentUser; @Before public void init() { devHelper.deleteAll(); requestPreprocessor = preprocessRequest( replaceUriHost("https", "p.hsl.fi", -1), removeHeaders("Host", "Content-Length")); responsePreprocessor = preprocessResponse( removeHeaders("Pragma", "Expires", "Cache-Control", "Content-Length")); documentationHandler = document("{method-name}", requestPreprocessor, responsePreprocessor); mockMvc = MockMvcBuilders.webAppContextSetup(context) .apply(documentationConfiguration(restDocumentation)) .alwaysDo(documentationHandler) .build(); facilityId = dummies.createFacility(); authToken = loginApiUserForFacility(facilityId); } @Before public void setTimezone() { originalDateTimeZone = DateTimeZone.getDefault(); DateTimeZone.setDefault(DateTimeZone.forID("Europe/Helsinki")); } @After public void restoreTimezone() { DateTimeZone.setDefault(originalDateTimeZone); } @Test public void jsonDefaultExample() throws Exception { MockHttpServletRequestBuilder request = get(UrlSchema.FACILITIES); mockMvc.perform(request) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)); } @Test public void jsonSuffixExample() throws Exception { MockHttpServletRequestBuilder request = get(UrlSchema.FACILITIES + ".json"); mockMvc.perform(request) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)); } @Test public void geojsonSuffixExample() throws Exception { MockHttpServletRequestBuilder request = get(UrlSchema.FACILITIES + ".geojson"); mockMvc.perform(request) .andExpect(status().isOk()) .andExpect(content().contentType(UrlSchema.GEOJSON)); } @Test public void jsonHeaderExample() throws Exception { MockHttpServletRequestBuilder request = get(UrlSchema.FACILITIES) .accept(MediaType.APPLICATION_JSON); mockMvc.perform(request) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON)); } @Test public void geojsonHeaderExample() throws Exception { dummies.createHub(); MockHttpServletRequestBuilder request = get(UrlSchema.HUBS) .accept(UrlSchema.GEOJSON); mockMvc.perform(request) .andExpect(status().isOk()) .andExpect(content().contentType(UrlSchema.GEOJSON)); } @Test public void limitOffsetSortExample() throws Exception { MockHttpServletRequestBuilder request = get(UrlSchema.FACILITIES) .param("limit", "10") .param("offset", "4") .param("sort.by", "name.fi") .param("sort.dir", Sort.Dir.ASC.name()); // XXX: not really testing whether all the parameters work mockMvc.perform(request) .andExpect(status().isOk()) .andExpect(jsonPath("results", is(empty()))) .andExpect(jsonPath("hasMore", is(false))); } @Test public void authenticationExample() throws Exception { MockHttpServletRequestBuilder request = put(UrlSchema.FACILITY_UTILIZATION, facilityId) .header("Authorization", "Bearer " + authToken) .contentType(MediaType.APPLICATION_JSON) .content("[]"); mockMvc.perform(request) .andExpect(status().isOk()); } @Test public void usageTrackingExample() throws Exception { MockHttpServletRequestBuilder request = get(UrlSchema.FACILITIES) .header("Liipi-Application-Id", "liipi-ui") .contentType(MediaType.APPLICATION_JSON) .content("[]"); mockMvc.perform(request) .andExpect(status().isOk()); } @Test public void facilityGeojsonExample() throws Exception { MockHttpServletRequestBuilder request = get(UrlSchema.FACILITY + ".geojson", facilityId); this.mockMvc.perform(request) .andExpect(status().isOk()); } @Test public void facilityExample() throws Exception { MockHttpServletRequestBuilder request = get(UrlSchema.FACILITY, facilityId); mockMvc.perform(request) .andExpect(status().isOk()) .andDo(documentationHandler.document(responseFields( fieldWithPath("id").description("Facility ID: `/api/v1/facilities/{id}`"), fieldWithPath("name").description("Localized name"), fieldWithPath("location").description("Facility location, GeoJSON Polygon"), fieldWithPath("operatorId").description("Facility operator ID: `/api/v1/operators/{operatorId}`"), fieldWithPath("status").description("Status, one of `/api/v1/facility-statuses`"), fieldWithPath("pricingMethod").description("Pricing method, one of `/api/v1/pricing-methods`"), fieldWithPath("statusDescription").description("Localized description of status (OPTIONAL)"), fieldWithPath("builtCapacity").description("Built capacity by CapacityType, may be split or shared by different Usage types as defined by pricing"), fieldWithPath("usages").description("Read-only summary of distinct pricing rows' usages"), fieldWithPath("pricing[].usage").description("Usage type, one of `/api/v1/usages`"), fieldWithPath("pricing[].capacityType").description("Capacity type, one of `/api/v1/capacity-types`"), fieldWithPath("pricing[].maxCapacity").description("Max capacity"), fieldWithPath("pricing[].dayType").description("Day type, one of `/api/v1/day-types`"), fieldWithPath("pricing[].time.from").description("Start time `hh[:mm]`"), fieldWithPath("pricing[].time.until").description("End time `hh[:mm]`, exclusive"), fieldWithPath("pricing[].price").description("Localized description of price (OPTIONAL)"), fieldWithPath("unavailableCapacities[].capacityType").description("Unavailable capacity type"), fieldWithPath("unavailableCapacities[].usage").description("Unavailable usage"), fieldWithPath("unavailableCapacities[].capacity").description("Unavailable capacity"), fieldWithPath("aliases").description("Alternative labels, list of strings"), fieldWithPath("ports[].location").description("Port location, GeoJSON Point"), fieldWithPath("ports[].entry").description("Entry for cars"), fieldWithPath("ports[].exit").description("Exit for cars"), fieldWithPath("ports[].pedestrian").description("Entry/exit by foot"), fieldWithPath("ports[].bicycle").description("Entry/exit for bicycles"), fieldWithPath("ports[].address").description("Port address (OPTIONAL)"), fieldWithPath("ports[].info").description("Localized port info (OPTIONAL)"), fieldWithPath("services").description("List of services provided, `/api/v1/services`"), fieldWithPath("contacts.emergency").description("Emergency contact ID, `/api/v1/contacts/{contactId}`"), fieldWithPath("contacts.operator").description("Operator contact ID, `/api/v1/contacts/{contactId}`"), fieldWithPath("contacts.service").description("Service contact ID, `/api/v1/contacts/{contactId}`"), fieldWithPath("paymentInfo.detail").description("Localized details of payment options (OPTIONAL)"), fieldWithPath("paymentInfo.url").description("Localized link to payment options (OPTIONAL)"), fieldWithPath("paymentInfo.paymentMethods").description("Allowed payment methods"), fieldWithPath("openingHours.openNow").description("Read-only summary of whether the facility is open right now"), fieldWithPath("openingHours.byDayType").description("Read-only summary of pricing rows' opening hours"), fieldWithPath("openingHours.info").description("Localized info about opening hours (OPTIONAL)"), fieldWithPath("openingHours.url").description("Localized link to opening hours info (OPTIONAL)")))); } @Test public void enumerationCapacityTypesExample() throws Exception { mockMvc.perform(get(UrlSchema.CAPACITY_TYPES)) .andExpect(status().isOk()); } @Test public void enumerationUsagesExample() throws Exception { mockMvc.perform(get(UrlSchema.USAGES)) .andExpect(status().isOk()); } @Test public void enumerationDayTypesExample() throws Exception { mockMvc.perform(get(UrlSchema.DAY_TYPES)) .andExpect(status().isOk()); } @Test public void enumerationServicesExample() throws Exception { mockMvc.perform(get(UrlSchema.SERVICES)) .andExpect(status().isOk()); } @Test public void enumerationPaymentMethodsExample() throws Exception { mockMvc.perform(get(UrlSchema.PAYMENT_METHODS)) .andExpect(status().isOk()); } @Test public void enumerationFacilityStatusesExample() throws Exception { mockMvc.perform(get(UrlSchema.FACILITY_STATUSES)) .andExpect(status().isOk()); } @Test public void enumerationPricingMethodsExample() throws Exception { mockMvc.perform(get(UrlSchema.PRICING_METHODS)) .andExpect(status().isOk()); } @Test public void allOperatorsExample() throws Exception { mockMvc.perform(get(UrlSchema.OPERATORS)) .andExpect(status().isOk()); } @Test public void operatorDetailsExample() throws Exception { mockMvc.perform(get(UrlSchema.OPERATOR, 1)) .andExpect(status().isOk()) .andDo(documentationHandler.document(responseFields( fieldWithPath("id").description("Contact ID: `/api/v1/operators/{id}`"), fieldWithPath("name").description("Localized name")))); } @Test public void allContactsExample() throws Exception { mockMvc.perform(get(UrlSchema.CONTACTS)) .andExpect(status().isOk()); } @Test public void findContactsByIdsExample() throws Exception { // XXX: not really testing whether all the parameters work mockMvc.perform(get(UrlSchema.CONTACTS).param("ids", "101", "102")) .andExpect(status().isOk()); } @Test public void findContactsByOperatorIdExample() throws Exception { // XXX: not really testing whether all the parameters work mockMvc.perform(get(UrlSchema.CONTACTS).param("operatorId", "42")) .andExpect(status().isOk()); } @Test public void contactDetailsExample() throws Exception { mockMvc.perform(get(UrlSchema.CONTACT, 1)) .andExpect(status().isOk()) .andDo(documentationHandler.document(responseFields( fieldWithPath("id").description("Contact ID: `/api/v1/contacts/{id}`"), fieldWithPath("name").description("Localized name"), fieldWithPath("operatorId").type(NUMBER).description("Contact operator ID: `/api/v1/operators/{operatorId}` (OPTIONAL)"), fieldWithPath("email").description("Email address (OPTIONAL)"), fieldWithPath("phone").description("Phone number (OPTIONAL)"), fieldWithPath("address").description("Contact address (OPTIONAL)"), fieldWithPath("address.streetAddress").description("Localized street name (OPTIONAL)"), fieldWithPath("address.postalCode").description("Postal code (OPTIONAL)"), fieldWithPath("address.city").description("Localized city name (OPTIONAL)"), fieldWithPath("openingHours").description("Localized opening hours (OPTIONAL)"), fieldWithPath("info").description("Localized information (OPTIONAL)")))); } @Test public void findFacilitiesByStatusesExample() throws Exception { // XXX: not really testing whether all the parameters work mockMvc.perform(get(UrlSchema.FACILITIES).param("statuses", FacilityStatus.IN_OPERATION.name(), FacilityStatus.EXCEPTIONAL_SITUATION.name())) .andExpect(status().isOk()); } @Test public void findFacilitiesByIdsExample() throws Exception { // XXX: not really testing whether all the parameters work mockMvc.perform(get(UrlSchema.FACILITIES).param("ids", "11", "12")) .andExpect(status().isOk()); } @Test public void findFacilitiesByGeometryExample() throws Exception { // XXX: not really testing whether all the parameters work String geometry = FacilityDaoTest.OVERLAPPING_AREA.asText(); geometry = geometry.substring(geometry.indexOf(";") + 1); mockMvc.perform(get(UrlSchema.FACILITIES).param("geometry", geometry)) .andExpect(status().isOk()); } @Test public void findFacilitiesByGeometryMaxDistanceExample() throws Exception { // XXX: not really testing whether all the parameters work String geometry = FacilityDaoTest.OVERLAPPING_AREA.asText(); geometry = geometry.substring(geometry.indexOf(";") + 1); mockMvc.perform(get(UrlSchema.FACILITIES).param("geometry", geometry).param("maxDistance", "123.45")) .andExpect(status().isOk()); } @Test public void findFacilitiesSummaryExample() throws Exception { // XXX: not really testing whether all the parameters work mockMvc.perform(get(UrlSchema.FACILITIES).param("summary", "true")) .andExpect(status().isOk()); } @Test public void utilizationExample() throws Exception { facilityService.registerUtilization(facilityId, Collections.singletonList(newUtilization()), currentUser); mockMvc.perform(get(UrlSchema.FACILITY_UTILIZATION, facilityId)) .andExpect(status().isOk()) .andExpect(jsonPath("[*]", hasSize(1))) .andDo(documentationHandler.document(responseFields( fieldWithPath("[]facilityId").description("The facility"), fieldWithPath("[]capacityType").description("The capacity type"), fieldWithPath("[]usage").description("The usage"), fieldWithPath("[]timestamp").description("When this information was last updated"), fieldWithPath("[]spacesAvailable").description("Number of available parking spaces for this facility, capacity type and usage combination"), fieldWithPath("[]capacity").description("Number of parking spaces (both reserved and available) for this facility, capacity type and usage combination")))); } @Test public void utilizationManyUsagesExample() throws Exception { List<Utilization> utilizations = utilizationsOfManyUsages(); initializePricingForUtilizations(facilityId, utilizations); facilityService.registerUtilization(facilityId, utilizations, currentUser); mockMvc.perform(get(UrlSchema.FACILITY_UTILIZATION, facilityId)) .andExpect(status().isOk()) .andExpect(jsonPath("[*]", hasSize(2))); } @Test public void utilizationsBatchExample() throws Exception { Facility facility1 = facilityService.getFacility(facilityId); Facility facility2 = dummies.createFacility(facility1.operatorId, facility1.contacts); facility2.id = facilityRepository.insertFacility(facility2); facilityService.registerUtilization(facility1.id, Collections.singletonList(newUtilization()), currentUser); facilityService.registerUtilization(facility2.id, Collections.singletonList(newUtilization()), currentUser); mockMvc.perform(get(UrlSchema.UTILIZATIONS, facilityId)) .andExpect(status().isOk()) .andExpect(jsonPath("[*]", hasSize(2))); } @Test public void utilizationUpdateExample() throws Exception { MockHttpServletRequestBuilder request = put(UrlSchema.FACILITY_UTILIZATION, facilityId) .header("Authorization", "Bearer " + authToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(Collections.singletonList(newUtilization()))); mockMvc.perform(request) .andExpect(status().isOk()) .andExpect(jsonPath("[*]", hasSize(1))); } @Test public void utilizationUpdateManyExample() throws Exception { List<Utilization> utilizations = utilizationsOfManyUsages(); initializePricingForUtilizations(facilityId, utilizations); MockHttpServletRequestBuilder request = put(UrlSchema.FACILITY_UTILIZATION, facilityId) .header("Authorization", "Bearer " + authToken) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(utilizations)); mockMvc.perform(request) .andExpect(status().isOk()) .andExpect(jsonPath("[*]", hasSize(2))); } @Test public void predictionExample() throws Exception { final long hubId = dummies.createHub(facilityId); facilityService.registerUtilization(facilityId, Collections.singletonList(newUtilization()), currentUser); predictionService.updatePredictions(); mockMvc.perform(get(UrlSchema.FACILITY_PREDICTION_ABSOLUTE, facilityId, new DateTime().plusHours(1).plusMinutes(30))) .andExpect(status().isOk()) .andDo(document("prediction-absolute-example", requestPreprocessor, responsePreprocessor)); mockMvc.perform(get(UrlSchema.FACILITY_PREDICTION_RELATIVE, facilityId, "01:30")) .andExpect(status().isOk()) .andDo(document("prediction-relative-example-hhmm", requestPreprocessor, responsePreprocessor)); mockMvc.perform(get(UrlSchema.FACILITY_PREDICTION_RELATIVE, facilityId, "90")) .andExpect(status().isOk()) .andDo(document("prediction-relative-example-minutes", requestPreprocessor, responsePreprocessor)); mockMvc.perform(get(UrlSchema.HUB_PREDICTION_ABSOLUTE, hubId, new DateTime().plusHours(1).plusMinutes(30))) .andExpect(status().isOk()) .andDo(document("hub-prediction-absolute-example", requestPreprocessor, responsePreprocessor)); mockMvc.perform(get(UrlSchema.HUB_PREDICTION_RELATIVE, hubId, "01:30")) .andExpect(status().isOk()) .andDo(document("hub-prediction-relative-example-hhmm", requestPreprocessor, responsePreprocessor)); mockMvc.perform(get(UrlSchema.HUB_PREDICTION_RELATIVE, hubId, "90")) .andExpect(status().isOk()) .andDo(document("hub-prediction-relative-example-minutes", requestPreprocessor, responsePreprocessor)); } @Test public void allHubsExample() throws Exception { dummies.createHub(); mockMvc.perform(get(UrlSchema.HUBS)) .andExpect(status().isOk()); } @Test public void hubGeojsonExample() throws Exception { long hubId = dummies.createHub(); mockMvc.perform(get(UrlSchema.HUB + ".geojson", hubId)) .andExpect(status().isOk()); } @Test public void hubDetailsExample() throws Exception { long hubId = dummies.createHub(); mockMvc.perform(get(UrlSchema.HUB, hubId)) .andExpect(status().isOk()) .andDo(documentationHandler.document(responseFields( fieldWithPath("id").description("Hub ID: `/api/v1/hubs/{id}`"), fieldWithPath("name").description("Localized name"), fieldWithPath("location").description("Hub location, GeoJSON Point"), fieldWithPath("address").description("Hub address (OPTIONAL)"), fieldWithPath("address.streetAddress").description("Localized street name (OPTIONAL)"), fieldWithPath("address.postalCode").description("Postal code (OPTIONAL)"), fieldWithPath("address.city").description("Localized city name (OPTIONAL)"), fieldWithPath("facilityIds").description("A list of facility IDs (number), `/api/v1/facilities/{id}`")))); } @Test public void findHubsByIdsExample() throws Exception { // XXX: not really testing whether all the parameters work mockMvc.perform(get(UrlSchema.HUBS).param("ids", "11", "12")) .andExpect(status().isOk()); } @Test public void findHubsByFacilityIdsExample() throws Exception { // XXX: not really testing whether all the parameters work mockMvc.perform(get(UrlSchema.HUBS).param("facilityIds", "11", "12")) .andExpect(status().isOk()); } @Test public void findHubsByGeometryExample() throws Exception { // XXX: not really testing whether all the parameters work String geometry = FacilityDaoTest.OVERLAPPING_AREA.asText(); geometry = geometry.substring(geometry.indexOf(";") + 1); mockMvc.perform(get(UrlSchema.HUBS).param("geometry", geometry)) .andExpect(status().isOk()); } @Test public void findHubsByGeometryMaxDistanceExample() throws Exception { // XXX: not really testing whether all the parameters work String geometry = FacilityDaoTest.OVERLAPPING_AREA.asText(); geometry = geometry.substring(geometry.indexOf(";") + 1); mockMvc.perform(get(UrlSchema.HUBS).param("geometry", geometry).param("maxDistance", "123.45")) .andExpect(status().isOk()); } // helpers public String loginApiUserForFacility(long facilityId) { Facility facility = facilityRepository.getFacility(facilityId); devHelper.deleteUsers(); currentUser = new NewUser(1L, "dummyoperator", OPERATOR_API, facility.operatorId, "secret"); devHelper.createOrUpdateUser(currentUser); return devHelper.login("dummyoperator").token; } public static Utilization newUtilization() { Utilization u = new Utilization(); u.capacityType = CapacityType.CAR; u.usage = Usage.PARK_AND_RIDE; u.spacesAvailable = 30; u.capacity = 50; u.timestamp = new DateTime(); return u; } public static List<Utilization> utilizationsOfManyUsages() { Utilization u1 = newUtilization(); u1.usage = Usage.HSL_TRAVEL_CARD; u1.spacesAvailable = 351; u1.capacity = 400; Utilization u2 = newUtilization(); u2.usage = Usage.COMMERCIAL; u2.spacesAvailable = 786; u2.capacity = 800; return Arrays.asList(u1, u2); } public void initializePricingForUtilizations(long facilityId, List<Utilization> utilizations) { Facility facility = facilityRepository.getFacility(facilityId); for (Utilization u : utilizations) { facility.pricing.add(new Pricing(u.capacityType, u.usage, 1000, DayType.BUSINESS_DAY, "00:00", "24:00", "")); } facilityRepository.updateFacility(facilityId, facility); } }