diff --git a/README.md b/README.md index 5c3ff04..733b4c0 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,18 @@ When the submodule is updated, to get the newest version of inserter you need to ] } ``` +- `GET /timetables/to-combine`: Fetch the vehicle schedule frame ID slated to be the target for combine, + considering the combine action with the staging vehicle schedule frame ID and target priority. + - Request params: + - `stagingVehicleScheduleFrameId` The ID of the staging vehicle schedule frame. Example: `"50f939b0-aac3-453a-b2f5-24c0cdf8ad21"` + - `targetPriority` The priority to which the staging timetables will be promoted. Example: `10` + + Example response body: + ```JSON + { + "toCombineTargetVehicleScheduleFrameId": "d3d0aea6-db3f-4421-b4eb-39cffe8835a8" + } + ``` ## Technical Documentation jore4-timetables-api is a Spring Boot application written in Kotlin, which implements a REST API for accessing the timetables database and creating more complicated updates in one transaction than is possible with the graphQL interface. diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt b/src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt index 7754288..54e872a 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/api/TimetablesController.kt @@ -92,6 +92,10 @@ class TimetablesController( val toReplaceVehicleScheduleFrameIds: List ) + data class ToCombineTimetablesResponseBody( + val toCombineTargetVehicleScheduleFrameId: UUID + ) + class TargetPriorityParsingException(message: String, val targetPriority: Int) : RuntimeException(message) @GetMapping("to-replace") @@ -117,6 +121,34 @@ class TimetablesController( .body(ToReplaceTimetablesResponseBody(toReplaceVehicleScheduleFrameIds = vehicleScheduleFrameIds)) } + @GetMapping("to-combine") + fun getTargetFrameIdsForCombine( + @RequestParam + targetPriority: Int, + @RequestParam + stagingVehicleScheduleFrameId: UUID + ): ResponseEntity { + LOGGER.info { "ToCombine api, stagingVehicleScheduleFrameId: $stagingVehicleScheduleFrameId, targetPriority: $targetPriority" } + + val targetPriorityEnumResult = runCatching { TimetablesPriority.fromInt(targetPriority) } + if (targetPriorityEnumResult.isFailure) { + throw TargetPriorityParsingException("Failed to parse target priority", targetPriority) + } + + val targetVehicleScheduleFrame = combineTimetablesService.fetchTargetVehicleScheduleFrame( + stagingVehicleScheduleFrameId, + targetPriorityEnumResult.getOrThrow() + ) + + return ResponseEntity.status(HttpStatus.OK) + .body( + ToCombineTimetablesResponseBody( + // ID of an existing row, can never be null. + toCombineTargetVehicleScheduleFrameId = targetVehicleScheduleFrame.vehicleScheduleFrameId!! + ) + ) + } + @ExceptionHandler(RuntimeException::class) fun handleRuntimeException(ex: RuntimeException): ResponseEntity { val hasuraExtensions: HasuraErrorExtensions = when (ex) { diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/config/WebSecurityConfig.kt b/src/main/kotlin/fi/hsl/jore4/timetables/config/WebSecurityConfig.kt index a276444..eddc537 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/config/WebSecurityConfig.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/config/WebSecurityConfig.kt @@ -25,6 +25,7 @@ class WebSecurityConfig { HttpMethod.GET, "/actuator/health", "/error", + "/timetables/to-combine", "/timetables/to-replace" ) .permitAll() diff --git a/src/main/kotlin/fi/hsl/jore4/timetables/service/CombineTimetablesService.kt b/src/main/kotlin/fi/hsl/jore4/timetables/service/CombineTimetablesService.kt index b02afd2..24aa7ae 100644 --- a/src/main/kotlin/fi/hsl/jore4/timetables/service/CombineTimetablesService.kt +++ b/src/main/kotlin/fi/hsl/jore4/timetables/service/CombineTimetablesService.kt @@ -58,6 +58,15 @@ class CombineTimetablesService( return targetVehicleScheduleFrame.vehicleScheduleFrameId!! // ID of an existing row, can never be null. } + @Transactional(readOnly = true) + fun fetchTargetVehicleScheduleFrame( + stagingVehicleScheduleFrameId: UUID, + targetPriority: TimetablesPriority + ): VehicleScheduleFrame { + val stagingVehicleScheduleFrame = fetchStagingVehicleScheduleFrame(stagingVehicleScheduleFrameId) + return fetchTargetVehicleScheduleFrame(stagingVehicleScheduleFrame, targetPriority) + } + private fun fetchTargetVehicleScheduleFrame( stagingVehicleScheduleFrame: VehicleScheduleFrame, targetPriority: TimetablesPriority diff --git a/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesToCombineApiTest.kt b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesToCombineApiTest.kt new file mode 100644 index 0000000..3d9aaa0 --- /dev/null +++ b/src/test/kotlin/fi/hsl/jore4/timetables/api/TimetablesToCombineApiTest.kt @@ -0,0 +1,118 @@ +package fi.hsl.jore4.timetables.api + +import com.ninjasquad.springmockk.MockkBean +import fi.hsl.jore.jore4.jooq.vehicle_schedule.tables.pojos.VehicleScheduleFrame +import fi.hsl.jore4.timetables.enumerated.TimetablesPriority +import fi.hsl.jore4.timetables.service.CombineTimetablesService +import io.mockk.every +import io.mockk.junit5.MockKExtension +import io.mockk.verify +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.http.MediaType +import org.springframework.test.context.ActiveProfiles +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.ResultActions +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status +import java.time.LocalDate +import java.util.UUID + +@ExtendWith(MockKExtension::class) +@AutoConfigureMockMvc +@SpringBootTest +@ActiveProfiles("test") +class TimetablesToCombineApiTest(@Autowired val mockMvc: MockMvc) { + @MockkBean + private lateinit var combineTimetablesService: CombineTimetablesService + + private val defaultTargetFrame = VehicleScheduleFrame( + vehicleScheduleFrameId = UUID.fromString("379076ee-d595-47e3-8050-2610d594b57c"), + validityStart = LocalDate.now(), + validityEnd = LocalDate.now(), + priority = 20, + label = "label" + ) + private val defaultToCombineTargetId = defaultTargetFrame.vehicleScheduleFrameId + private fun executeToCombineTimetablesRequest( + stagingFrameId: UUID, + targetPriority: Int + ): ResultActions { + return mockMvc.perform( + MockMvcRequestBuilders.get("/timetables/to-combine") + .contentType(MediaType.APPLICATION_JSON) + .param("stagingVehicleScheduleFrameId", stagingFrameId.toString()) + .param("targetPriority", targetPriority.toString()) + ) + } + + @Test + fun `returns 200 and correct response when called successfully`() { + val stagingVehicleScheduleFrameId = UUID.fromString("81f109d1-dbe2-412a-996e-aa510416b2e4") + val targetPriority = 10 + + every { + combineTimetablesService.fetchTargetVehicleScheduleFrame( + stagingVehicleScheduleFrameId, + TimetablesPriority.fromInt(targetPriority) + ) + } answers { defaultTargetFrame } + + executeToCombineTimetablesRequest(stagingVehicleScheduleFrameId, targetPriority) + .andExpect(status().isOk) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + content().json( + """ + { + "toCombineTargetVehicleScheduleFrameId": $defaultToCombineTargetId + } + """.trimIndent(), + true + ) + ) + + verify(exactly = 1) { + combineTimetablesService.fetchTargetVehicleScheduleFrame( + stagingVehicleScheduleFrameId, + TimetablesPriority.fromInt(targetPriority) + ) + } + } + + @Test + fun `throws a 400 error when parsing target priority fails`() { + val errorMessage = "Failed to parse target priority" + val stagingVehicleScheduleFrameId = UUID.fromString("023281cd-51e9-4544-a2af-7b7e268e3a3a") + val invalidTargetPriorityInput = 9999 + + executeToCombineTimetablesRequest(stagingVehicleScheduleFrameId, invalidTargetPriorityInput) + .andExpect(status().isBadRequest) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect( + content().json( + """ + { + "message": "$errorMessage", + "extensions": { + "code": 400, + "type": "TargetPriorityParsingError", + "targetPriority": $invalidTargetPriorityInput + } + } + """.trimIndent(), + true + ) + ) + verify(exactly = 0) { + combineTimetablesService.fetchTargetVehicleScheduleFrame( + stagingVehicleScheduleFrameId, + any() + ) + } + } +}