1
1
package com .conveyal .gtfs .loader ;
2
2
3
3
import com .conveyal .gtfs .TestUtils ;
4
+ import com .conveyal .gtfs .util .CalendarDTO ;
4
5
import com .conveyal .gtfs .util .FareDTO ;
5
6
import com .conveyal .gtfs .util .FareRuleDTO ;
6
7
import com .conveyal .gtfs .util .FeedInfoDTO ;
8
+ import com .conveyal .gtfs .util .FrequencyDTO ;
7
9
import com .conveyal .gtfs .util .InvalidNamespaceException ;
10
+ import com .conveyal .gtfs .util .PatternDTO ;
11
+ import com .conveyal .gtfs .util .PatternStopDTO ;
8
12
import com .conveyal .gtfs .util .RouteDTO ;
13
+ import com .conveyal .gtfs .util .ShapePointDTO ;
14
+ import com .conveyal .gtfs .util .StopDTO ;
15
+ import com .conveyal .gtfs .util .StopTimeDTO ;
16
+ import com .conveyal .gtfs .util .TripDTO ;
9
17
import com .fasterxml .jackson .databind .ObjectMapper ;
10
18
import org .junit .AfterClass ;
11
19
import org .junit .BeforeClass ;
24
32
import static org .hamcrest .Matchers .equalTo ;
25
33
import static org .junit .Assert .assertThat ;
26
34
35
+ /**
36
+ * This class contains CRUD tests for {@link JdbcTableWriter} (i.e., editing GTFS entities in the RDBMS). Set up
37
+ * consists of creating a scratch database and an empty feed snapshot, which is the necessary starting condition
38
+ * for building a GTFS feed from scratch. It then runs the various CRUD tests and finishes by dropping the database
39
+ * (even if tests fail).
40
+ */
27
41
public class JDBCTableWriterTest {
28
42
29
43
private static final Logger LOG = LoggerFactory .getLogger (JDBCTableWriterTest .class );
30
44
31
45
private static String testDBName ;
32
46
private static DataSource testDataSource ;
33
47
private static String testNamespace ;
48
+ private static String simpleServiceId = "1" ;
49
+ private static String firstStopId = "1" ;
50
+ private static String lastStopId = "2" ;
34
51
private static final ObjectMapper mapper = new ObjectMapper ();
35
52
36
53
private static JdbcTableWriter createTestTableWriter (Table table ) throws InvalidNamespaceException {
37
54
return new JdbcTableWriter (table , testDataSource , testNamespace );
38
55
}
39
56
40
57
@ BeforeClass
41
- public static void setUpClass () throws SQLException {
42
- // create a new database
58
+ public static void setUpClass () throws SQLException , IOException , InvalidNamespaceException {
59
+ // Create a new database
43
60
testDBName = TestUtils .generateNewDB ();
44
61
String dbConnectionUrl = String .format ("jdbc:postgresql://localhost/%s" , testDBName );
45
62
testDataSource = createDataSource (dbConnectionUrl , null , null );
@@ -51,10 +68,13 @@ public static void setUpClass() throws SQLException {
51
68
"snapshot_of varchar)" );
52
69
connection .commit ();
53
70
LOG .info ("feeds table created" );
54
-
55
- // create an empty snapshot to create a new namespace and all the tables
71
+ // Create an empty snapshot to create a new namespace and all the tables
56
72
FeedLoadResult result = makeSnapshot (null , testDataSource );
57
73
testNamespace = result .uniqueIdentifier ;
74
+ // Create a service calendar and two stops, both of which are necessary to perform pattern and trip tests.
75
+ createWeekdayCalendar (simpleServiceId , "20180103" , "20180104" );
76
+ createSimpleStop (firstStopId , "First Stop" , 34.2222 , -87.333 );
77
+ createSimpleStop (lastStopId , "Last Stop" , 34.2233 , -87.334 );
58
78
}
59
79
60
80
@ Test
@@ -120,6 +140,10 @@ public void canCreateUpdateAndDeleteFeedInfoEntities() throws IOException, SQLEx
120
140
));
121
141
}
122
142
143
+ /**
144
+ * Ensure that potentially malicious SQL injection is sanitized properly during create operations.
145
+ * TODO: We might should perform this check on multiple entities and for update and/or delete operations.
146
+ */
123
147
@ Test
124
148
public void canPreventSQLInjection () throws IOException , SQLException , InvalidNamespaceException {
125
149
// create new object to be saved
@@ -248,23 +272,8 @@ public void canCreateUpdateAndDeleteRoutes() throws IOException, SQLException, I
248
272
final Class <RouteDTO > routeDTOClass = RouteDTO .class ;
249
273
250
274
// create new object to be saved
251
- RouteDTO routeInput = new RouteDTO ();
252
275
String routeId = "500" ;
253
- routeInput .route_id = routeId ;
254
- routeInput .agency_id = "RTA" ;
255
- // Empty value should be permitted for transfers and transfer_duration
256
- routeInput .route_short_name = "500" ;
257
- routeInput .route_long_name = "Hollingsworth" ;
258
- routeInput .route_type = 3 ;
259
-
260
- // convert object to json and save it
261
- JdbcTableWriter createTableWriter = createTestTableWriter (routeTable );
262
- String createOutput = createTableWriter .create (mapper .writeValueAsString (routeInput ), true );
263
- LOG .info ("create {} output:" , routeTable .name );
264
- LOG .info (createOutput );
265
-
266
- // parse output
267
- RouteDTO createdRoute = mapper .readValue (createOutput , routeDTOClass );
276
+ RouteDTO createdRoute = createSimpleTestRoute (routeId , "RTA" , "500" , "Hollingsworth" , 3 );
268
277
269
278
// make sure saved data matches expected data
270
279
assertThat (createdRoute .route_id , equalTo (routeId ));
@@ -309,6 +318,169 @@ public void canCreateUpdateAndDeleteRoutes() throws IOException, SQLException, I
309
318
));
310
319
}
311
320
321
+ /**
322
+ * Create and store a simple route for testing.
323
+ */
324
+ private static RouteDTO createSimpleTestRoute (String routeId , String agencyId , String shortName , String longName , int routeType ) throws InvalidNamespaceException , IOException , SQLException {
325
+ RouteDTO input = new RouteDTO ();
326
+ input .route_id = routeId ;
327
+ input .agency_id = agencyId ;
328
+ // Empty value should be permitted for transfers and transfer_duration
329
+ input .route_short_name = shortName ;
330
+ input .route_long_name = longName ;
331
+ input .route_type = routeType ;
332
+ // convert object to json and save it
333
+ JdbcTableWriter createTableWriter = createTestTableWriter (Table .ROUTES );
334
+ String output = createTableWriter .create (mapper .writeValueAsString (input ), true );
335
+ LOG .info ("create {} output:" , Table .ROUTES .name );
336
+ LOG .info (output );
337
+ // parse output
338
+ return mapper .readValue (output , RouteDTO .class );
339
+ }
340
+
341
+ /**
342
+ * Creates a pattern by first creating a route and then a pattern for that route.
343
+ */
344
+ private static PatternDTO createSimpleRouteAndPattern (String routeId , String patternId , String name ) throws InvalidNamespaceException , SQLException , IOException {
345
+ // Create new route
346
+ createSimpleTestRoute (routeId , "RTA" , "500" , "Hollingsworth" , 3 );
347
+ // Create new pattern for route
348
+ PatternDTO input = new PatternDTO ();
349
+ input .pattern_id = patternId ;
350
+ input .route_id = routeId ;
351
+ input .name = name ;
352
+ input .use_frequency = 0 ;
353
+ input .shapes = new ShapePointDTO []{};
354
+ input .pattern_stops = new PatternStopDTO []{};
355
+ // Write the pattern to the database
356
+ JdbcTableWriter createPatternWriter = createTestTableWriter (Table .PATTERNS );
357
+ String output = createPatternWriter .create (mapper .writeValueAsString (input ), true );
358
+ LOG .info ("create {} output:" , Table .PATTERNS .name );
359
+ LOG .info (output );
360
+ // Parse output
361
+ return mapper .readValue (output , PatternDTO .class );
362
+ }
363
+
364
+ /**
365
+ * Test that a frequency trip entry CANNOT be added for a timetable-based pattern. Expects an exception to be thrown.
366
+ */
367
+ @ Test (expected = IllegalStateException .class )
368
+ public void cannotCreateFrequencyForTimetablePattern () throws InvalidNamespaceException , IOException , SQLException {
369
+ PatternDTO simplePattern = createSimpleRouteAndPattern ("900" , "8" , "The Loop" );
370
+ TripDTO tripInput = constructFrequencyTrip (simplePattern .pattern_id , simplePattern .route_id , 6 * 60 * 60 );
371
+ JdbcTableWriter createTripWriter = createTestTableWriter (Table .TRIPS );
372
+ createTripWriter .create (mapper .writeValueAsString (tripInput ), true );
373
+ }
374
+
375
+ /**
376
+ * Checks that creating a frequency trip functions properly. This also updates the pattern to include pattern stops,
377
+ * which is a prerequisite for creating a frequency trip with stop times.
378
+ */
379
+ @ Test
380
+ public void canCreateUpdateAndDeleteFrequencyTripForFrequencyPattern () throws IOException , SQLException , InvalidNamespaceException {
381
+ // Store Table and Class values for use in test.
382
+ final Table tripsTable = Table .TRIPS ;
383
+ int startTime = 6 * 60 * 60 ;
384
+ PatternDTO simplePattern = createSimpleRouteAndPattern ("1000" , "9" , "The Line" );
385
+ TripDTO tripInput = constructFrequencyTrip (simplePattern .pattern_id , simplePattern .route_id , startTime );
386
+ JdbcTableWriter createTripWriter = createTestTableWriter (tripsTable );
387
+ // Update pattern with pattern stops, set to use frequencies, and TODO shape points
388
+ JdbcTableWriter patternUpdater = createTestTableWriter (Table .PATTERNS );
389
+ simplePattern .use_frequency = 1 ;
390
+ simplePattern .pattern_stops = new PatternStopDTO []{
391
+ new PatternStopDTO (simplePattern .pattern_id , firstStopId , 0 ),
392
+ new PatternStopDTO (simplePattern .pattern_id , lastStopId , 1 )
393
+ };
394
+ String updatedPatternOutput = patternUpdater .update (simplePattern .id , mapper .writeValueAsString (simplePattern ), true );
395
+ LOG .info ("Updated pattern output: {}" , updatedPatternOutput );
396
+ // Create new trip for the pattern
397
+ String createTripOutput = createTripWriter .create (mapper .writeValueAsString (tripInput ), true );
398
+ TripDTO createdTrip = mapper .readValue (createTripOutput , TripDTO .class );
399
+ // Update trip
400
+ // TODO: Add update and delete tests for updating pattern stops, stop_times, and frequencies.
401
+ String updatedTripId = "100A" ;
402
+ createdTrip .trip_id = updatedTripId ;
403
+ JdbcTableWriter updateTripWriter = createTestTableWriter (tripsTable );
404
+ String updateTripOutput = updateTripWriter .update (tripInput .id , mapper .writeValueAsString (createdTrip ), true );
405
+ TripDTO updatedTrip = mapper .readValue (updateTripOutput , TripDTO .class );
406
+ // Check that saved data matches expected data
407
+ assertThat (updatedTrip .frequencies [0 ].start_time , equalTo (startTime ));
408
+ assertThat (updatedTrip .trip_id , equalTo (updatedTripId ));
409
+ // Delete trip record
410
+ JdbcTableWriter deleteTripWriter = createTestTableWriter (tripsTable );
411
+ int deleteOutput = deleteTripWriter .delete (
412
+ createdTrip .id ,
413
+ true
414
+ );
415
+ LOG .info ("deleted {} records from {}" , deleteOutput , tripsTable .name );
416
+ // Check that record does not exist in DB
417
+ assertThatSqlQueryYieldsZeroRows (
418
+ String .format (
419
+ "select * from %s.%s where id=%d" ,
420
+ testNamespace ,
421
+ tripsTable .name ,
422
+ createdTrip .id
423
+ ));
424
+ }
425
+
426
+ /**
427
+ * Construct (without writing to the database) a trip with a frequency entry.
428
+ */
429
+ private TripDTO constructFrequencyTrip (String patternId , String routeId , int startTime ) {
430
+ TripDTO tripInput = new TripDTO ();
431
+ tripInput .pattern_id = patternId ;
432
+ tripInput .route_id = routeId ;
433
+ tripInput .service_id = simpleServiceId ;
434
+ tripInput .stop_times = new StopTimeDTO []{
435
+ new StopTimeDTO (firstStopId , 0 , 0 , 0 ),
436
+ new StopTimeDTO (lastStopId , 60 , 60 , 1 )
437
+ };
438
+ FrequencyDTO frequency = new FrequencyDTO ();
439
+ frequency .start_time = startTime ;
440
+ frequency .end_time = 9 * 60 * 60 ;
441
+ frequency .headway_secs = 15 * 60 ;
442
+ tripInput .frequencies = new FrequencyDTO []{frequency };
443
+ return tripInput ;
444
+ }
445
+
446
+ /**
447
+ * Create and store a simple stop entity.
448
+ */
449
+ private static StopDTO createSimpleStop (String stopId , String stopName , double latitude , double longitude ) throws InvalidNamespaceException , IOException , SQLException {
450
+ JdbcTableWriter createStopWriter = new JdbcTableWriter (Table .STOPS , testDataSource , testNamespace );
451
+ StopDTO input = new StopDTO ();
452
+ input .stop_id = stopId ;
453
+ input .stop_name = stopName ;
454
+ input .stop_lat = latitude ;
455
+ input .stop_lon = longitude ;
456
+ String output = createStopWriter .create (mapper .writeValueAsString (input ), true );
457
+ LOG .info ("create {} output:" , Table .STOPS .name );
458
+ LOG .info (output );
459
+ return mapper .readValue (output , StopDTO .class );
460
+ }
461
+
462
+ /**
463
+ * Create and store a simple calendar that runs on each weekday.
464
+ */
465
+ private static CalendarDTO createWeekdayCalendar (String serviceId , String startDate , String endDate ) throws IOException , SQLException , InvalidNamespaceException {
466
+ JdbcTableWriter createCalendarWriter = new JdbcTableWriter (Table .CALENDAR , testDataSource , testNamespace );
467
+ CalendarDTO calendarInput = new CalendarDTO ();
468
+ calendarInput .service_id = serviceId ;
469
+ calendarInput .monday = 1 ;
470
+ calendarInput .tuesday = 1 ;
471
+ calendarInput .wednesday = 1 ;
472
+ calendarInput .thursday = 1 ;
473
+ calendarInput .friday = 1 ;
474
+ calendarInput .saturday = 0 ;
475
+ calendarInput .sunday = 0 ;
476
+ calendarInput .start_date = startDate ;
477
+ calendarInput .end_date = endDate ;
478
+ String output = createCalendarWriter .create (mapper .writeValueAsString (calendarInput ), true );
479
+ LOG .info ("create {} output:" , Table .CALENDAR .name );
480
+ LOG .info (output );
481
+ return mapper .readValue (output , CalendarDTO .class );
482
+ }
483
+
312
484
@ AfterClass
313
485
public static void tearDownClass () {
314
486
TestUtils .dropDB (testDBName );
0 commit comments