Skip to content

Commit d47eea5

Browse files
renovate[bot]jarkkoka
authored andcommitted
Fix timing point validation.
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
1 parent 0a25fe0 commit d47eea5

3 files changed

Lines changed: 264 additions & 6 deletions

File tree

pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<graphql-kotlin.version>6.4.1</graphql-kotlin.version>
3333
<ktor.version>2.0.3</ktor.version> <!-- must be in sync with graphql-kotlin.version -->
3434
<jackson-datatype.version>2.15.1</jackson-datatype.version>
35+
<mockk.version>1.12.4</mockk.version>
3536

3637
<!-- Other properties -->
3738
<start.class>fi.hsl.jore4.hastus.HastusApplicationKt</start.class>
@@ -536,6 +537,13 @@
536537
<scope>test</scope>
537538
</dependency>
538539

540+
<dependency>
541+
<groupId>io.mockk</groupId>
542+
<artifactId>mockk</artifactId>
543+
<version>${mockk.version}</version>
544+
<scope>test</scope>
545+
</dependency>
546+
539547
<dependency>
540548
<groupId>org.quicktheories</groupId>
541549
<artifactId>quicktheories</artifactId>

src/main/kotlin/fi/hsl/jore4/hastus/export/ExportService.kt

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package fi.hsl.jore4.hastus.export
33
import fi.hsl.jore4.hastus.data.hastus.IHastusData
44
import fi.hsl.jore4.hastus.data.jore.JoreDistanceBetweenTwoStopPoints
55
import fi.hsl.jore4.hastus.data.jore.JoreLine
6+
import fi.hsl.jore4.hastus.data.jore.JoreRouteScheduledStop
67
import fi.hsl.jore4.hastus.data.jore.JoreScheduledStop
78
import fi.hsl.jore4.hastus.data.jore.JoreTimingPlace
89
import fi.hsl.jore4.hastus.data.mapper.ConversionsToHastus
@@ -80,20 +81,24 @@ class ExportService @Autowired constructor(
8081
}
8182
}
8283

83-
if (route.stopsOnRoute.first().timingPlaceShortName == null) {
84+
val firstStopOnRoute: JoreRouteScheduledStop = route.stopsOnRoute.first()
85+
86+
if (!firstStopOnRoute.isTimingPoint || firstStopOnRoute.timingPlaceShortName == null) {
8487
LOGGER.warn {
85-
"The first stop point of the journey pattern for route ${route.label} is not a timing " +
86-
"point as mandated by Hastus"
88+
"The first stop point of the journey pattern for route ${route.label} is not a valid " +
89+
"timing point as mandated by Hastus"
8790
}
8891
if (failOnTimingPointValidation) {
8992
throw FirstStopNotTimingPointException(route.label)
9093
}
9194
}
9295

93-
if (route.stopsOnRoute.last().timingPlaceShortName == null) {
96+
val lastStopOnRoute: JoreRouteScheduledStop = route.stopsOnRoute.last()
97+
98+
if (!lastStopOnRoute.isTimingPoint || lastStopOnRoute.timingPlaceShortName == null) {
9499
LOGGER.warn {
95-
"The last stop point of the journey pattern for route ${route.label} is not a timing " +
96-
"point as mandated by Hastus"
100+
"The last stop point of the journey pattern for route ${route.label} is not a valid " +
101+
"timing point as mandated by Hastus"
97102
}
98103
if (failOnTimingPointValidation) {
99104
throw LastStopNotTimingPointException(route.label)
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
package fi.hsl.jore4.hastus.export
2+
3+
import fi.hsl.jore4.hastus.data.jore.JoreLine
4+
import fi.hsl.jore4.hastus.data.jore.JoreRoute
5+
import fi.hsl.jore4.hastus.data.jore.JoreRouteScheduledStop
6+
import fi.hsl.jore4.hastus.graphql.FetchRoutesResult
7+
import fi.hsl.jore4.hastus.graphql.GraphQLService
8+
import io.mockk.every
9+
import io.mockk.impl.annotations.MockK
10+
import io.mockk.junit5.MockKExtension
11+
import org.junit.jupiter.api.BeforeEach
12+
import org.junit.jupiter.api.DisplayName
13+
import org.junit.jupiter.api.Nested
14+
import org.junit.jupiter.api.Test
15+
import org.junit.jupiter.api.extension.ExtendWith
16+
import java.time.LocalDate
17+
import kotlin.test.assertFailsWith
18+
19+
@ExtendWith(MockKExtension::class)
20+
class ExportServiceTest {
21+
22+
@MockK
23+
lateinit var graphQLService: GraphQLService
24+
25+
lateinit var exportService: ExportService
26+
27+
@BeforeEach
28+
fun setupServiceUnderTest() {
29+
exportService = ExportService(graphQLService, true)
30+
}
31+
32+
@DisplayName("Validate deep-fetched routes got from GraphQLService.deepFetchRoutes(...)")
33+
@Nested
34+
inner class ValidateDeepFetchedRoutes {
35+
36+
private fun stubDeepFetchRoutesForValidationSideEffects(line: JoreLine): FetchRoutesResult {
37+
val fetchRoutesResult = FetchRoutesResult(listOf(line), emptyList(), emptyList(), emptyList())
38+
39+
// given
40+
every {
41+
graphQLService.deepFetchRoutes(any(), any(), any(), any())
42+
} /* then */ returns fetchRoutesResult
43+
44+
return fetchRoutesResult
45+
}
46+
47+
private fun invokeExportRoutesWithAnyParameters() {
48+
// Because of the stubbing done in stubDeepFetchRoutesForValidationSideEffects() the
49+
// parameter values used here are not meaningful. Anything goes.
50+
exportService.exportRoutes(listOf(), 10, LocalDate.now(), emptyMap())
51+
}
52+
53+
@DisplayName("Validation should succeed when the first and the last stop points are timing points")
54+
@Test
55+
fun smoke() {
56+
val stopPoints = listOf(
57+
createFirstStopPoint("1KALA"),
58+
createFirstStopPoint("1ELIEL")
59+
)
60+
val line = createLine(stopPoints)
61+
62+
stubDeepFetchRoutesForValidationSideEffects(line)
63+
64+
invokeExportRoutesWithAnyParameters()
65+
}
66+
67+
@DisplayName("When the journey pattern consists of less than two stop points")
68+
@Nested
69+
inner class WhenThereAreLessThanTwoStopPoints {
70+
71+
@DisplayName("When there is only one stop point in journey pattern")
72+
@Test
73+
fun whenFirstStopPointIsNotTimingPointAndDoesNotHaveTimingPlaceAssociation() {
74+
val line = createLine(
75+
listOf(
76+
createFirstStopPoint("1KALA")
77+
// no other stop points given, just one
78+
)
79+
)
80+
81+
stubDeepFetchRoutesForValidationSideEffects(line)
82+
83+
assertFailsWith<TooFewStopPointsException> {
84+
invokeExportRoutesWithAnyParameters()
85+
}
86+
}
87+
}
88+
89+
@DisplayName("When the first stop point in journey pattern is not a valid timing point")
90+
@Nested
91+
inner class WhenFirstStopPointIsNotTimingPoint {
92+
93+
@DisplayName("When the first stop point is not a timing point and does not have timing place name")
94+
@Test
95+
fun whenFirstStopPointIsNotTimingPointAndDoesNotHaveTimingPlaceName() {
96+
val stopPoints = listOf(
97+
createFirstStopPoint(null, false),
98+
createLastStopPoint("1ELIEL")
99+
)
100+
val line = createLine(stopPoints)
101+
102+
stubDeepFetchRoutesForValidationSideEffects(line)
103+
104+
assertFailsWith<FirstStopNotTimingPointException> {
105+
invokeExportRoutesWithAnyParameters()
106+
}
107+
}
108+
109+
@DisplayName("When the first stop point is a timing point but does not have timing place name")
110+
@Test
111+
fun whenFirstStopPointIsTimingPointButDoesNotHaveTimingPlaceName() {
112+
val stopPoints = listOf(
113+
createFirstStopPoint(null, true),
114+
createLastStopPoint("1ELIEL")
115+
)
116+
val line = createLine(stopPoints)
117+
118+
stubDeepFetchRoutesForValidationSideEffects(line)
119+
120+
assertFailsWith<FirstStopNotTimingPointException> {
121+
invokeExportRoutesWithAnyParameters()
122+
}
123+
}
124+
125+
@DisplayName("When the first stop point is not a timing point but has timing place name")
126+
@Test
127+
fun whenFirstStopPointIsNotTimingPointButHasTimingPlaceName() {
128+
val stopPoints = listOf(
129+
createFirstStopPoint("1KALA", false),
130+
createLastStopPoint("1ELIEL")
131+
)
132+
val line = createLine(stopPoints)
133+
134+
stubDeepFetchRoutesForValidationSideEffects(line)
135+
136+
assertFailsWith<FirstStopNotTimingPointException> {
137+
invokeExportRoutesWithAnyParameters()
138+
}
139+
}
140+
}
141+
142+
@DisplayName("When the last stop point in journey pattern is not a valid timing point")
143+
@Nested
144+
inner class WhenLastStopPointIsNotTimingPoint {
145+
146+
@DisplayName("When the last stop point is not a timing point and does not have timing place name")
147+
@Test
148+
fun whenLastStopPointIsNotTimingPointAndDoesNotHaveTimingPlaceName() {
149+
val stopPoints = listOf(
150+
createFirstStopPoint("1KALA"),
151+
createLastStopPoint(null, false)
152+
)
153+
val line = createLine(stopPoints)
154+
155+
stubDeepFetchRoutesForValidationSideEffects(line)
156+
157+
assertFailsWith<LastStopNotTimingPointException> {
158+
invokeExportRoutesWithAnyParameters()
159+
}
160+
}
161+
162+
@DisplayName("When the last stop point is a timing point but does not have timing place name")
163+
@Test
164+
fun whenLastStopPointIsTimingPointButDoesNotHaveTimingPlaceName() {
165+
val stopPoints = listOf(
166+
createFirstStopPoint("1ELIEL"),
167+
createLastStopPoint(null, true)
168+
)
169+
val line = createLine(stopPoints)
170+
171+
stubDeepFetchRoutesForValidationSideEffects(line)
172+
173+
assertFailsWith<LastStopNotTimingPointException> {
174+
invokeExportRoutesWithAnyParameters()
175+
}
176+
}
177+
178+
@DisplayName("When the last stop point is not a timing point but has timing place name")
179+
@Test
180+
fun whenLastStopPointIsNotTimingPointButHasTimingPlaceName() {
181+
val stopPoints = listOf(
182+
createFirstStopPoint("1KALA"),
183+
createLastStopPoint("1ELIEL", false)
184+
)
185+
val line = createLine(stopPoints)
186+
187+
stubDeepFetchRoutesForValidationSideEffects(line)
188+
189+
assertFailsWith<LastStopNotTimingPointException> {
190+
invokeExportRoutesWithAnyParameters()
191+
}
192+
}
193+
}
194+
}
195+
196+
companion object {
197+
198+
fun createLine(stopsOnRoute: List<JoreRouteScheduledStop>): JoreLine {
199+
return JoreLine(
200+
label = "65",
201+
"Rautatientori - Veräjälaakso FI",
202+
0,
203+
listOf(
204+
JoreRoute(
205+
label = "65x",
206+
variant = "",
207+
uniqueLabel = "65x",
208+
name = "Reitti A - B FI",
209+
direction = 1,
210+
reversible = false,
211+
stopsOnRoute = stopsOnRoute
212+
)
213+
)
214+
)
215+
}
216+
217+
fun createFirstStopPoint(
218+
timingPlaceShortName: String?,
219+
isTimingPoint: Boolean = true
220+
): JoreRouteScheduledStop {
221+
return JoreRouteScheduledStop(
222+
timingPlaceShortName = timingPlaceShortName,
223+
distanceToNextStop = 123.0,
224+
isRegulatedTimingPoint = false,
225+
isAllowedLoad = false,
226+
isTimingPoint = isTimingPoint,
227+
stopLabel = "H1000"
228+
)
229+
}
230+
231+
fun createLastStopPoint(
232+
timingPlaceShortName: String?,
233+
isTimingPoint: Boolean = true
234+
): JoreRouteScheduledStop {
235+
return JoreRouteScheduledStop(
236+
timingPlaceShortName = timingPlaceShortName,
237+
distanceToNextStop = 0.0,
238+
isRegulatedTimingPoint = false,
239+
isAllowedLoad = false,
240+
isTimingPoint = isTimingPoint,
241+
stopLabel = "H9999"
242+
)
243+
}
244+
}
245+
}

0 commit comments

Comments
 (0)