diff --git a/api/src/scripts/populate_db_gtfs.py b/api/src/scripts/populate_db_gtfs.py index 86d1b0276..efc581a7c 100644 --- a/api/src/scripts/populate_db_gtfs.py +++ b/api/src/scripts/populate_db_gtfs.py @@ -122,6 +122,31 @@ def process_entity_types(self, session: "Session", feed: Gtfsrealtimefeed, row, self.logger.warning(f"Entity types array is empty for feed {stable_id}") feed.entitytypes.clear() + def inherit_static_feed_locations(self, gtfs_rt_feed, matched_feeds): + """ + Inherit locations from referenced static GTFS feeds when the GTFS-RT + feed has no location metadata of its own. + + Existing GTFS-RT locations are preserved. When multiple static feeds + are referenced, their locations are combined and deduplicated by + location ID. + """ + if gtfs_rt_feed.locations: + return + + inherited_locations = [] + seen_location_ids = set() + + for gtfs_feed in matched_feeds: + for location in gtfs_feed.locations: + if location.id in seen_location_ids: + continue + seen_location_ids.add(location.id) + inherited_locations.append(location) + + if inherited_locations: + gtfs_rt_feed.locations = inherited_locations + def process_feed_references(self, session: "Session"): """ Process the feed references @@ -155,6 +180,7 @@ def process_feed_references(self, session: "Session"): matched_feeds.append(gtfs_feed) gtfs_rt_feed.gtfs_feeds = matched_feeds + self.inherit_static_feed_locations(gtfs_rt_feed, matched_feeds) session.add(gtfs_rt_feed) session.flush() self.logger.info( diff --git a/api/tests/integration/populate_tests/test_data/sources_test.csv b/api/tests/integration/populate_tests/test_data/sources_test.csv index 6bbd6a46f..0a18a4aba 100644 --- a/api/tests/integration/populate_tests/test_data/sources_test.csv +++ b/api/tests/integration/populate_tests/test_data/sources_test.csv @@ -19,3 +19,10 @@ mdb_source_id,data_type,entity_type,location.country_code,location.subdivision_n # Also change is_producer_url_unstable from base TRUE to FALSE: should change to False. # Also change is_seasonal from base TRUE to FALSE: should change to False. 1563,gtfs-rt,tu,US,SomeState,SomeCity,SomeCity Bus,FALSE,RT,,,mdb-50,http://bar.com,0,,,,,,,,,,inactive,,10,,FALSE,FALSE + +# Regression fixture for #1567: a new GTFS-RT feed with no location fields references an existing static feed that already has location metadata. +99991,gtfs-rt,tu,,,,Location Lifecycle Test,TRUE,Locationless RT,,,50,https://example.com/gtfs-rt,0,,,,,,,,,,active,,,,, + + +# Multi-parent regression fixture for #1567: inherit the union of locations from referenced static GTFS feeds. +99992,gtfs-rt,tu,,,,Location Lifecycle Test,TRUE,Multi-parent Locationless RT,,,40|50,https://example.com/gtfs-rt-multi,0,,,,,,,,,,active,,,,, diff --git a/api/tests/integration/populate_tests/test_populate.py b/api/tests/integration/populate_tests/test_populate.py index 072c51cdf..8dd64ebb2 100644 --- a/api/tests/integration/populate_tests/test_populate.py +++ b/api/tests/integration/populate_tests/test_populate.py @@ -232,3 +232,92 @@ def test_is_feed_reference_overwrite(client: TestClient): ) json_response = response.json() assert json_response["feed_references"] == ["mdb-50"] + + +def test_new_gtfs_rt_feed_inherits_existing_static_feed_location(client: TestClient): + """ + Regression test for mobility-database-catalogs #1567. + + A newly imported GTFS-RT feed with no location fields of its own should not + remain locationless when its static_reference points to a GTFS feed that + already has location metadata. + """ + response = client.request( + "GET", + "/v1/gtfs_rt_feeds/mdb-99991", + headers=authHeaders, + ) + + assert response.status_code == 200 + feed = response.json() + + # First prove that static_reference was resolved successfully. + assert feed["feed_references"] == ["mdb-50"] + + # mdb-50 already has CA / Ontario / Barrie in the layered populate fixture. + # The suspected bug is that this location is not synchronized when the + # GTFS-RT -> GTFS relationship is subsequently established. + assert len(feed["locations"]) == 1 + assert feed["locations"][0]["country_code"] == "CA" + assert feed["locations"][0]["subdivision_name"] == "Ontario" + assert feed["locations"][0]["municipality"] == "Barrie" + + +def test_gtfs_rt_feed_with_existing_location_is_not_overwritten(client: TestClient): + """ + A GTFS-RT location explicitly present in the catalogue must not be replaced + by the location of its referenced static GTFS feed. + """ + expected_locations = { + "mdb-1562": ("CA", "BC", "Vancouver"), + "mdb-1563": ("US", "SomeState", "SomeCity"), + } + + for feed_id, expected in expected_locations.items(): + response = client.request( + "GET", + f"/v1/gtfs_rt_feeds/{feed_id}", + headers=authHeaders, + ) + + assert response.status_code == 200 + feed = response.json() + + assert feed["feed_references"] == ["mdb-50"] + assert len(feed["locations"]) == 1 + assert feed["locations"][0]["country_code"] == expected[0] + assert feed["locations"][0]["subdivision_name"] == expected[1] + assert feed["locations"][0]["municipality"] == expected[2] + + +def test_locationless_gtfs_rt_feed_inherits_union_of_static_feed_locations( + client: TestClient, +): + """ + Multiple static references contribute a deduplicated union of locations to + a locationless GTFS-RT feed. + """ + response = client.request( + "GET", + "/v1/gtfs_rt_feeds/mdb-99992", + headers=authHeaders, + ) + + assert response.status_code == 200 + feed = response.json() + + assert set(feed["feed_references"]) == {"mdb-40", "mdb-50"} + + locations = { + ( + location["country_code"], + location["subdivision_name"], + location["municipality"], + ) + for location in feed["locations"] + } + + assert locations == { + ("CA", "Ontario", "London"), + ("CA", "Ontario", "Barrie"), + }