Skip to content

Commit

Permalink
Fix timing point validation.
Browse files Browse the repository at this point in the history
There was a bug where it was only validated if the first and the last stop point have a timing place association. The fix involves validating that the first and the last stop point are used as timing points in the journey pattern.

Resolves HSLdevcom/jore4#1339
  • Loading branch information
renovate[bot] authored and jarkkoka committed Jun 28, 2023
1 parent 0a25fe0 commit d47eea5
Show file tree
Hide file tree
Showing 3 changed files with 264 additions and 6 deletions.
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<graphql-kotlin.version>6.4.1</graphql-kotlin.version>
<ktor.version>2.0.3</ktor.version> <!-- must be in sync with graphql-kotlin.version -->
<jackson-datatype.version>2.15.1</jackson-datatype.version>
<mockk.version>1.12.4</mockk.version>

<!-- Other properties -->
<start.class>fi.hsl.jore4.hastus.HastusApplicationKt</start.class>
Expand Down Expand Up @@ -536,6 +537,13 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>${mockk.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.quicktheories</groupId>
<artifactId>quicktheories</artifactId>
Expand Down
17 changes: 11 additions & 6 deletions src/main/kotlin/fi/hsl/jore4/hastus/export/ExportService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fi.hsl.jore4.hastus.export
import fi.hsl.jore4.hastus.data.hastus.IHastusData
import fi.hsl.jore4.hastus.data.jore.JoreDistanceBetweenTwoStopPoints
import fi.hsl.jore4.hastus.data.jore.JoreLine
import fi.hsl.jore4.hastus.data.jore.JoreRouteScheduledStop
import fi.hsl.jore4.hastus.data.jore.JoreScheduledStop
import fi.hsl.jore4.hastus.data.jore.JoreTimingPlace
import fi.hsl.jore4.hastus.data.mapper.ConversionsToHastus
Expand Down Expand Up @@ -80,20 +81,24 @@ class ExportService @Autowired constructor(
}
}

if (route.stopsOnRoute.first().timingPlaceShortName == null) {
val firstStopOnRoute: JoreRouteScheduledStop = route.stopsOnRoute.first()

if (!firstStopOnRoute.isTimingPoint || firstStopOnRoute.timingPlaceShortName == null) {
LOGGER.warn {
"The first stop point of the journey pattern for route ${route.label} is not a timing " +
"point as mandated by Hastus"
"The first stop point of the journey pattern for route ${route.label} is not a valid " +
"timing point as mandated by Hastus"
}
if (failOnTimingPointValidation) {
throw FirstStopNotTimingPointException(route.label)
}
}

if (route.stopsOnRoute.last().timingPlaceShortName == null) {
val lastStopOnRoute: JoreRouteScheduledStop = route.stopsOnRoute.last()

if (!lastStopOnRoute.isTimingPoint || lastStopOnRoute.timingPlaceShortName == null) {
LOGGER.warn {
"The last stop point of the journey pattern for route ${route.label} is not a timing " +
"point as mandated by Hastus"
"The last stop point of the journey pattern for route ${route.label} is not a valid " +
"timing point as mandated by Hastus"
}
if (failOnTimingPointValidation) {
throw LastStopNotTimingPointException(route.label)
Expand Down
245 changes: 245 additions & 0 deletions src/test/kotlin/fi/hsl/jore4/hastus/export/ExportServiceTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package fi.hsl.jore4.hastus.export

import fi.hsl.jore4.hastus.data.jore.JoreLine
import fi.hsl.jore4.hastus.data.jore.JoreRoute
import fi.hsl.jore4.hastus.data.jore.JoreRouteScheduledStop
import fi.hsl.jore4.hastus.graphql.FetchRoutesResult
import fi.hsl.jore4.hastus.graphql.GraphQLService
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import java.time.LocalDate
import kotlin.test.assertFailsWith

@ExtendWith(MockKExtension::class)
class ExportServiceTest {

@MockK
lateinit var graphQLService: GraphQLService

lateinit var exportService: ExportService

@BeforeEach
fun setupServiceUnderTest() {
exportService = ExportService(graphQLService, true)
}

@DisplayName("Validate deep-fetched routes got from GraphQLService.deepFetchRoutes(...)")
@Nested
inner class ValidateDeepFetchedRoutes {

private fun stubDeepFetchRoutesForValidationSideEffects(line: JoreLine): FetchRoutesResult {
val fetchRoutesResult = FetchRoutesResult(listOf(line), emptyList(), emptyList(), emptyList())

// given
every {
graphQLService.deepFetchRoutes(any(), any(), any(), any())
} /* then */ returns fetchRoutesResult

return fetchRoutesResult
}

private fun invokeExportRoutesWithAnyParameters() {
// Because of the stubbing done in stubDeepFetchRoutesForValidationSideEffects() the
// parameter values used here are not meaningful. Anything goes.
exportService.exportRoutes(listOf(), 10, LocalDate.now(), emptyMap())
}

@DisplayName("Validation should succeed when the first and the last stop points are timing points")
@Test
fun smoke() {
val stopPoints = listOf(
createFirstStopPoint("1KALA"),
createFirstStopPoint("1ELIEL")
)
val line = createLine(stopPoints)

stubDeepFetchRoutesForValidationSideEffects(line)

invokeExportRoutesWithAnyParameters()
}

@DisplayName("When the journey pattern consists of less than two stop points")
@Nested
inner class WhenThereAreLessThanTwoStopPoints {

@DisplayName("When there is only one stop point in journey pattern")
@Test
fun whenFirstStopPointIsNotTimingPointAndDoesNotHaveTimingPlaceAssociation() {
val line = createLine(
listOf(
createFirstStopPoint("1KALA")
// no other stop points given, just one
)
)

stubDeepFetchRoutesForValidationSideEffects(line)

assertFailsWith<TooFewStopPointsException> {
invokeExportRoutesWithAnyParameters()
}
}
}

@DisplayName("When the first stop point in journey pattern is not a valid timing point")
@Nested
inner class WhenFirstStopPointIsNotTimingPoint {

@DisplayName("When the first stop point is not a timing point and does not have timing place name")
@Test
fun whenFirstStopPointIsNotTimingPointAndDoesNotHaveTimingPlaceName() {
val stopPoints = listOf(
createFirstStopPoint(null, false),
createLastStopPoint("1ELIEL")
)
val line = createLine(stopPoints)

stubDeepFetchRoutesForValidationSideEffects(line)

assertFailsWith<FirstStopNotTimingPointException> {
invokeExportRoutesWithAnyParameters()
}
}

@DisplayName("When the first stop point is a timing point but does not have timing place name")
@Test
fun whenFirstStopPointIsTimingPointButDoesNotHaveTimingPlaceName() {
val stopPoints = listOf(
createFirstStopPoint(null, true),
createLastStopPoint("1ELIEL")
)
val line = createLine(stopPoints)

stubDeepFetchRoutesForValidationSideEffects(line)

assertFailsWith<FirstStopNotTimingPointException> {
invokeExportRoutesWithAnyParameters()
}
}

@DisplayName("When the first stop point is not a timing point but has timing place name")
@Test
fun whenFirstStopPointIsNotTimingPointButHasTimingPlaceName() {
val stopPoints = listOf(
createFirstStopPoint("1KALA", false),
createLastStopPoint("1ELIEL")
)
val line = createLine(stopPoints)

stubDeepFetchRoutesForValidationSideEffects(line)

assertFailsWith<FirstStopNotTimingPointException> {
invokeExportRoutesWithAnyParameters()
}
}
}

@DisplayName("When the last stop point in journey pattern is not a valid timing point")
@Nested
inner class WhenLastStopPointIsNotTimingPoint {

@DisplayName("When the last stop point is not a timing point and does not have timing place name")
@Test
fun whenLastStopPointIsNotTimingPointAndDoesNotHaveTimingPlaceName() {
val stopPoints = listOf(
createFirstStopPoint("1KALA"),
createLastStopPoint(null, false)
)
val line = createLine(stopPoints)

stubDeepFetchRoutesForValidationSideEffects(line)

assertFailsWith<LastStopNotTimingPointException> {
invokeExportRoutesWithAnyParameters()
}
}

@DisplayName("When the last stop point is a timing point but does not have timing place name")
@Test
fun whenLastStopPointIsTimingPointButDoesNotHaveTimingPlaceName() {
val stopPoints = listOf(
createFirstStopPoint("1ELIEL"),
createLastStopPoint(null, true)
)
val line = createLine(stopPoints)

stubDeepFetchRoutesForValidationSideEffects(line)

assertFailsWith<LastStopNotTimingPointException> {
invokeExportRoutesWithAnyParameters()
}
}

@DisplayName("When the last stop point is not a timing point but has timing place name")
@Test
fun whenLastStopPointIsNotTimingPointButHasTimingPlaceName() {
val stopPoints = listOf(
createFirstStopPoint("1KALA"),
createLastStopPoint("1ELIEL", false)
)
val line = createLine(stopPoints)

stubDeepFetchRoutesForValidationSideEffects(line)

assertFailsWith<LastStopNotTimingPointException> {
invokeExportRoutesWithAnyParameters()
}
}
}
}

companion object {

fun createLine(stopsOnRoute: List<JoreRouteScheduledStop>): JoreLine {
return JoreLine(
label = "65",
"Rautatientori - Veräjälaakso FI",
0,
listOf(
JoreRoute(
label = "65x",
variant = "",
uniqueLabel = "65x",
name = "Reitti A - B FI",
direction = 1,
reversible = false,
stopsOnRoute = stopsOnRoute
)
)
)
}

fun createFirstStopPoint(
timingPlaceShortName: String?,
isTimingPoint: Boolean = true
): JoreRouteScheduledStop {
return JoreRouteScheduledStop(
timingPlaceShortName = timingPlaceShortName,
distanceToNextStop = 123.0,
isRegulatedTimingPoint = false,
isAllowedLoad = false,
isTimingPoint = isTimingPoint,
stopLabel = "H1000"
)
}

fun createLastStopPoint(
timingPlaceShortName: String?,
isTimingPoint: Boolean = true
): JoreRouteScheduledStop {
return JoreRouteScheduledStop(
timingPlaceShortName = timingPlaceShortName,
distanceToNextStop = 0.0,
isRegulatedTimingPoint = false,
isAllowedLoad = false,
isTimingPoint = isTimingPoint,
stopLabel = "H9999"
)
}
}
}

0 comments on commit d47eea5

Please sign in to comment.