For return journeys, replace the single combined date navigation row with two separate rows so outbound and return dates can be adjusted independently. For inbound underground options, show one service before the earliest catchable (as an "aim for this" option) rather than the next service after it, which often arrived too late to connect with the GWR train. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
684 lines
28 KiB
Python
684 lines
28 KiB
Python
from datetime import datetime
|
|
|
|
import app as app_module
|
|
import trip_planner as trip_planner_module
|
|
|
|
|
|
def _client():
|
|
app_module.app.config['TESTING'] = True
|
|
return app_module.app.test_client()
|
|
|
|
|
|
def _stub_data(monkeypatch, prices=None, gwr_fares=None):
|
|
monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None)
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None)
|
|
monkeypatch.setattr(
|
|
app_module.rtt_scraper,
|
|
'fetch',
|
|
lambda travel_date, user_agent, station_crs='BRI': [
|
|
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.gwr_fares_scraper,
|
|
'fetch',
|
|
lambda station_crs, travel_date: gwr_fares or {'07:00': {'ticket': 'Anytime Day Single', 'price': 138.70, 'code': 'SDS'}},
|
|
)
|
|
p = (prices or {}).get('10:01', {})
|
|
monkeypatch.setattr(
|
|
app_module.eurostar_scraper,
|
|
'fetch',
|
|
lambda destination, travel_date: [
|
|
{
|
|
'depart_st_pancras': '10:01',
|
|
'arrive_destination': '13:34',
|
|
'destination': destination,
|
|
'train_number': 'ES 9014',
|
|
'price': p.get('price') if isinstance(p, dict) else None,
|
|
'seats': p.get('seats') if isinstance(p, dict) else None,
|
|
'plus_price': p.get('plus_price') if isinstance(p, dict) else None,
|
|
'plus_seats': p.get('plus_seats') if isinstance(p, dict) else None,
|
|
},
|
|
],
|
|
)
|
|
|
|
|
|
def test_index_shows_station_dropdown_and_destination_radios():
|
|
client = _client()
|
|
|
|
resp = client.get('/')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert 'Departure point' in html
|
|
assert 'Bristol Temple Meads' in html
|
|
assert 'name="station_crs"' in html
|
|
assert html.count('type="radio"') == len(app_module.DESTINATIONS)
|
|
assert 'destination-rotterdam' in html
|
|
|
|
|
|
def test_search_redirects_to_results_with_selected_params():
|
|
client = _client()
|
|
|
|
resp = client.get('/search?destination=rotterdam&travel_date=2026-04-10&min_connection=60&max_connection=120&station_crs=BRI')
|
|
|
|
assert resp.status_code == 302
|
|
assert resp.headers['Location'].endswith(
|
|
'/results/BRI/rotterdam/2026-04-10?min_connection=60&max_connection=120'
|
|
)
|
|
|
|
|
|
def test_search_redirects_return_with_return_date():
|
|
client = _client()
|
|
|
|
resp = client.get('/search?journey_type=return&destination=paris&travel_date=2026-04-10&return_date=2026-04-17&station_crs=BRI')
|
|
|
|
assert resp.status_code == 302
|
|
assert resp.headers['Location'].endswith(
|
|
'/results/BRI/paris/2026-04-10/return/2026-04-17'
|
|
)
|
|
|
|
|
|
def test_nr_weekday_cache_key_includes_timetable_period():
|
|
key = app_module._nr_weekday_cache_key("to_paddington", "BRI", "2026-06-22")
|
|
|
|
assert key == "weekday_rtt_to_paddington_BRI_2026-05-17_2026-12-12_mon"
|
|
|
|
|
|
def test_results_shows_same_day_destination_switcher(monkeypatch):
|
|
_stub_data(monkeypatch)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert 'Switch destination for Friday 10 April 2026' in html
|
|
assert '<span class="chip-current">Paris Gare du Nord</span>' in html
|
|
assert '/results/BRI/brussels/2026-04-10?min_connection=60&max_connection=120' in html
|
|
assert '/results/BRI/rotterdam/2026-04-10?min_connection=60&max_connection=120' in html
|
|
assert 'ES 9014' in html
|
|
|
|
|
|
def test_results_can_render_from_weekday_timetable_cache(monkeypatch):
|
|
travel_date = "2026-06-22"
|
|
cache = {
|
|
app_module._nr_weekday_cache_key("to_paddington", "BRI", travel_date): [
|
|
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
|
|
],
|
|
app_module._eurostar_weekday_cache_key("outbound", travel_date, "Paris Gare du Nord"): [
|
|
{'depart_st_pancras': '10:01', 'arrive_destination': '13:34',
|
|
'destination': 'Paris Gare du Nord', 'train_number': 'ES 9014'},
|
|
],
|
|
}
|
|
monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: cache.get(key))
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None)
|
|
monkeypatch.setattr(
|
|
app_module.rtt_scraper,
|
|
'fetch',
|
|
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should use weekday NR cache")),
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.eurostar_scraper,
|
|
'fetch',
|
|
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should use weekday Eurostar cache")),
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.gwr_fares_scraper,
|
|
'fetch',
|
|
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("should stream prices later")),
|
|
)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-06-22?min_connection=60&max_connection=120')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert '07:00 → 08:45' in html
|
|
assert '10:01 → 13:34' in html
|
|
assert 'ES 9014' in html
|
|
assert 'checking exact timetable' in html
|
|
assert '/api/results_refresh/BRI/paris/2026-06-22' in html
|
|
assert 'refreshFullResults()' in html
|
|
assert 'window.location.reload()' not in html
|
|
assert 'Eurostar prices not yet available' not in html
|
|
|
|
|
|
def test_results_refresh_reloads_when_exact_timetable_differs(monkeypatch):
|
|
travel_date = "2026-06-22"
|
|
cache = {
|
|
app_module._nr_weekday_cache_key("to_paddington", "BRI", travel_date): [
|
|
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
|
|
],
|
|
app_module._eurostar_weekday_cache_key("outbound", travel_date, "Paris Gare du Nord"): [
|
|
{'depart_st_pancras': '10:01', 'arrive_destination': '13:34',
|
|
'destination': 'Paris Gare du Nord', 'train_number': 'ES 9014'},
|
|
],
|
|
}
|
|
monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: cache.get(key))
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: cache.__setitem__(key, data))
|
|
monkeypatch.setattr(
|
|
app_module.rtt_scraper,
|
|
'fetch',
|
|
lambda travel_date, user_agent, station_crs='BRI': [
|
|
{'depart_bristol': '07:05', 'arrive_paddington': '08:50', 'headcode': '1A24'},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.eurostar_scraper,
|
|
'fetch',
|
|
lambda destination, travel_date: [
|
|
{'depart_st_pancras': '10:01', 'arrive_destination': '13:34',
|
|
'destination': destination, 'train_number': 'ES 9014',
|
|
'price': 59, 'seats': 42, 'plus_price': 89, 'plus_seats': 5},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.gwr_fares_scraper,
|
|
'fetch',
|
|
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("reload should stop before fare fetch")),
|
|
)
|
|
client = _client()
|
|
|
|
resp = client.get('/api/results_refresh/BRI/paris/2026-06-22')
|
|
body = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert '"type": "reload"' in body
|
|
assert cache[app_module._nr_exact_cache_key("to_paddington", "BRI", travel_date)][0]['depart_bristol'] == '07:05'
|
|
assert cache[app_module._nr_weekday_cache_key("to_paddington", "BRI", travel_date)][0]['depart_bristol'] == '07:05'
|
|
|
|
|
|
def test_results_refresh_streams_prices_when_timetable_matches(monkeypatch):
|
|
travel_date = "2026-06-22"
|
|
nr_timetable = [
|
|
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
|
|
]
|
|
es_timetable = [
|
|
{'depart_st_pancras': '10:01', 'arrive_destination': '13:34',
|
|
'destination': 'Paris Gare du Nord', 'train_number': 'ES 9014'},
|
|
]
|
|
cache = {
|
|
app_module._nr_weekday_cache_key("to_paddington", "BRI", travel_date): nr_timetable,
|
|
app_module._eurostar_weekday_cache_key("outbound", travel_date, "Paris Gare du Nord"): es_timetable,
|
|
}
|
|
monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: cache.get(key))
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: cache.__setitem__(key, data))
|
|
monkeypatch.setattr(
|
|
app_module.rtt_scraper,
|
|
'fetch',
|
|
lambda travel_date, user_agent, station_crs='BRI': nr_timetable,
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.eurostar_scraper,
|
|
'fetch',
|
|
lambda destination, travel_date: [
|
|
{**es_timetable[0], 'price': 59, 'seats': 42, 'plus_price': 89, 'plus_seats': 5},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.gwr_fares_scraper,
|
|
'fetch',
|
|
lambda station_crs, travel_date: {
|
|
'07:00': {'ticket': 'Anytime Day Single', 'price': 138.70, 'code': 'SDS'},
|
|
},
|
|
)
|
|
client = _client()
|
|
|
|
resp = client.get('/api/results_refresh/BRI/paris/2026-06-22')
|
|
body = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert '"type": "reload"' not in body
|
|
assert '"type": "eurostar_prices"' in body
|
|
assert '"main:10:01"' in body
|
|
assert '"price": 59' in body
|
|
assert '"type": "walkon_fares"' in body
|
|
assert '"price": 138.7' in body
|
|
|
|
|
|
def test_results_progressive_shell_loads_without_scraping(monkeypatch):
|
|
def fail_fetch(*args, **kwargs):
|
|
raise AssertionError("progressive shell should not fetch data")
|
|
|
|
monkeypatch.setattr(app_module.rtt_scraper, 'fetch', fail_fetch)
|
|
monkeypatch.setattr(app_module.eurostar_scraper, 'fetch', fail_fetch)
|
|
monkeypatch.setattr(app_module.gwr_fares_scraper, 'fetch', fail_fetch)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10?progressive=1')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert 'Loading train times and fares' in html
|
|
assert 'render=full' in html
|
|
|
|
|
|
def test_return_progressive_shell_formats_return_date(monkeypatch):
|
|
def fail_fetch(*args, **kwargs):
|
|
raise AssertionError("progressive shell should not fetch data")
|
|
|
|
monkeypatch.setattr(app_module.rtt_scraper, 'fetch', fail_fetch)
|
|
monkeypatch.setattr(app_module.eurostar_scraper, 'fetch_return', fail_fetch)
|
|
monkeypatch.setattr(app_module.gwr_fares_scraper, 'fetch', fail_fetch)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10/return/2026-04-17?progressive=1')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert 'Friday 10 April 2026 to Friday 17 April 2026' in html
|
|
assert 'to 2026-04-17' not in html
|
|
|
|
|
|
def test_results_title_and_social_meta_include_destination(monkeypatch):
|
|
_stub_data(monkeypatch)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/lille/2026-04-10?min_connection=60&max_connection=120')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert '<title>Bristol Temple Meads to Lille Europe via Eurostar</title>' in html
|
|
assert '<meta property="og:title" content="Bristol Temple Meads to Lille Europe via Eurostar">' in html
|
|
assert (
|
|
'<meta property="og:description" content="Train options from Bristol Temple Meads '
|
|
'to Lille Europe on Friday 10 April 2026 via Paddington, St Pancras, and Eurostar.">'
|
|
) in html
|
|
assert '<meta property="og:url" content="http://localhost/results/BRI/lille/2026-04-10?min_connection=60&max_connection=120">' in html
|
|
|
|
|
|
def test_results_marks_trips_within_five_minutes_of_fastest_and_slowest(monkeypatch):
|
|
monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None)
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None)
|
|
monkeypatch.setattr(app_module.gwr_fares_scraper, 'fetch', lambda s, d: {})
|
|
monkeypatch.setattr(
|
|
app_module.rtt_scraper,
|
|
'fetch',
|
|
lambda travel_date, user_agent, station_crs='BRI': [
|
|
{'depart_bristol': '07:00', 'arrive_paddington': '08:30', 'headcode': '1A01'},
|
|
{'depart_bristol': '07:05', 'arrive_paddington': '08:36', 'headcode': '1A02'},
|
|
{'depart_bristol': '07:10', 'arrive_paddington': '08:46', 'headcode': '1A03'},
|
|
{'depart_bristol': '07:15', 'arrive_paddington': '08:56', 'headcode': '1A04'},
|
|
{'depart_bristol': '07:20', 'arrive_paddington': '09:06', 'headcode': '1A05'},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.eurostar_scraper,
|
|
'fetch',
|
|
lambda destination, travel_date: [
|
|
{'depart_st_pancras': '09:30', 'arrive_destination': '11:50', 'destination': destination, 'train_number': 'ES 1001', 'price': None, 'seats': None},
|
|
{'depart_st_pancras': '09:40', 'arrive_destination': '12:00', 'destination': destination, 'train_number': 'ES 1002', 'price': None, 'seats': None},
|
|
{'depart_st_pancras': '09:50', 'arrive_destination': '12:20', 'destination': destination, 'train_number': 'ES 1003', 'price': None, 'seats': None},
|
|
{'depart_st_pancras': '10:00', 'arrive_destination': '12:35', 'destination': destination, 'train_number': 'ES 1004', 'price': None, 'seats': None},
|
|
{'depart_st_pancras': '10:10', 'arrive_destination': '12:45', 'destination': destination, 'train_number': 'ES 1005', 'price': None, 'seats': None},
|
|
],
|
|
)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert html.count('title="Fastest journey"') == 2
|
|
assert html.count('title="Slowest journey"') == 2
|
|
assert '4h 50m ⚡' in html
|
|
assert '4h 55m ⚡' in html
|
|
assert '5h 20m 🐢' in html
|
|
assert '5h 25m 🐢' in html
|
|
assert '5h 10m ⚡' not in html
|
|
assert '5h 10m 🐢' not in html
|
|
|
|
|
|
def test_results_shows_only_pre_first_reachable_unreachable_services(monkeypatch):
|
|
# GWR arrives 08:45; min=60 → earliest viable Eurostar 09:45; max=120 → latest 10:45.
|
|
# 09:30 too early → shown as "Too early"
|
|
# 10:15 reachable → shown as a trip (needs circle line XML, so not tested here)
|
|
# 12:30 after first reachable → hidden
|
|
monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None)
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None)
|
|
monkeypatch.setattr(app_module.gwr_fares_scraper, 'fetch', lambda s, d: {})
|
|
monkeypatch.setattr(
|
|
app_module.rtt_scraper,
|
|
'fetch',
|
|
lambda travel_date, user_agent, station_crs='BRI': [
|
|
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.eurostar_scraper,
|
|
'fetch',
|
|
lambda destination, travel_date: [
|
|
{'depart_st_pancras': '09:30', 'arrive_destination': '12:00', 'destination': destination, 'train_number': 'ES 9001', 'price': None, 'seats': None},
|
|
{'depart_st_pancras': '10:15', 'arrive_destination': '13:40', 'destination': destination, 'train_number': 'ES 9002', 'price': None, 'seats': None},
|
|
{'depart_st_pancras': '12:30', 'arrive_destination': '15:55', 'destination': destination, 'train_number': 'ES 9003', 'price': None, 'seats': None},
|
|
],
|
|
)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert 'ES 9001' in html # before first reachable → shown
|
|
assert 'Too early' in html
|
|
assert 'ES 9003' not in html # after first reachable → hidden
|
|
|
|
|
|
def test_results_shows_eurostar_price_and_total(monkeypatch):
|
|
# 07:00 on Friday 2026-04-10 → Anytime £138.70 walk-on + ES £59.00
|
|
_stub_data(monkeypatch, prices={'10:01': {'price': 59, 'seats': 42}})
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert '£59' in html # Eurostar Standard price
|
|
assert '£138.70' in html # Walk-on price shown in NR cell
|
|
# Total (£197.70) is computed client-side; verify data attributes carry the right values
|
|
assert 'data-walkon="138.7"' in html
|
|
assert 'data-es-std="59"' in html
|
|
|
|
|
|
def test_results_uses_unique_row_keys_for_same_eurostar(monkeypatch):
|
|
monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None)
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None)
|
|
monkeypatch.setattr(app_module.gwr_fares_scraper, 'fetch', lambda s, d: {
|
|
'07:00': {'ticket': 'Anytime Day Single', 'price': 138.70, 'code': 'SDS'},
|
|
'07:30': {'ticket': 'Anytime Day Single', 'price': 138.70, 'code': 'SDS'},
|
|
})
|
|
monkeypatch.setattr(
|
|
app_module.rtt_scraper,
|
|
'fetch',
|
|
lambda travel_date, user_agent, station_crs='BRI': [
|
|
{'depart_bristol': '07:00', 'arrive_paddington': '08:30', 'headcode': '1A01'},
|
|
{'depart_bristol': '07:30', 'arrive_paddington': '09:00', 'headcode': '1A02'},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.eurostar_scraper,
|
|
'fetch',
|
|
lambda destination, travel_date: [
|
|
{'depart_st_pancras': '10:01', 'arrive_destination': '13:34',
|
|
'destination': destination, 'train_number': 'ES 9014',
|
|
'price': 59, 'seats': 42, 'plus_price': 89, 'plus_seats': 5},
|
|
],
|
|
)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert 'data-row-key="main:07:00:10:01"' in html
|
|
assert 'data-row-key="main:07:30:10:01"' in html
|
|
assert '"main:07:00:10:01"' in html
|
|
assert '"main:07:30:10:01"' in html
|
|
|
|
|
|
def test_results_shows_unreachable_service_when_no_trips(monkeypatch):
|
|
# Only one Eurostar at 09:30; GWR arrives 08:45 with min=60 → unreachable.
|
|
# No trips at all, so the unreachable service is shown as "Too early".
|
|
monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None)
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None)
|
|
monkeypatch.setattr(app_module.gwr_fares_scraper, 'fetch', lambda s, d: {})
|
|
monkeypatch.setattr(
|
|
app_module.rtt_scraper,
|
|
'fetch',
|
|
lambda travel_date, user_agent, station_crs='BRI': [
|
|
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.eurostar_scraper,
|
|
'fetch',
|
|
lambda destination, travel_date: [
|
|
{'depart_st_pancras': '09:30', 'arrive_destination': '12:00', 'destination': destination, 'train_number': 'ES 9001', 'price': None, 'seats': None},
|
|
],
|
|
)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert 'ES 9001' in html
|
|
assert 'Too early' in html
|
|
assert 'No valid journeys found.' not in html
|
|
|
|
|
|
def test_results_shows_eurostar_plus_price(monkeypatch):
|
|
_stub_data(monkeypatch, prices={'10:01': {'price': 59, 'seats': 42, 'plus_price': 89, 'plus_seats': 5}})
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert '£59' in html # Standard price
|
|
assert '£89' in html # Plus price
|
|
assert 'Plus' in html # Plus label
|
|
|
|
|
|
def test_results_selectors_present(monkeypatch):
|
|
_stub_data(monkeypatch)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert 'nr-type-select' in html
|
|
assert 'es-type-select' in html
|
|
assert 'Load advance prices' in html
|
|
assert 'Plus' in html
|
|
|
|
|
|
def test_results_preloads_cached_advance_fares(monkeypatch):
|
|
advance_data = {
|
|
'07:00': {
|
|
'advance_std': {'ticket': 'Advance Single', 'price': 45.0, 'code': 'ADV'},
|
|
'advance_1st': None,
|
|
}
|
|
}
|
|
def fake_get_cached(key, ttl=None):
|
|
if 'gwr_advance' in key:
|
|
return advance_data
|
|
return None
|
|
monkeypatch.setattr(app_module, 'get_cached', fake_get_cached)
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None)
|
|
monkeypatch.setattr(
|
|
app_module.rtt_scraper, 'fetch',
|
|
lambda travel_date, user_agent, station_crs='BRI': [
|
|
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
|
|
],
|
|
)
|
|
monkeypatch.setattr(app_module.gwr_fares_scraper, 'fetch', lambda s, d: {})
|
|
monkeypatch.setattr(
|
|
app_module.eurostar_scraper, 'fetch',
|
|
lambda destination, travel_date: [
|
|
{'depart_st_pancras': '10:01', 'arrive_destination': '13:34',
|
|
'destination': destination, 'train_number': 'ES 9014',
|
|
'price': None, 'seats': None, 'plus_price': None, 'plus_seats': None},
|
|
],
|
|
)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10?min_connection=60&max_connection=120')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
# Cached advance fares are embedded in the page JS
|
|
assert '"advance_std"' in html
|
|
assert '45.0' in html
|
|
# Button is absent (hidden via cachedAdvanceFares check in JS)
|
|
# The JS will hide it on load; the data is present for applyAdvanceFares()
|
|
assert 'cachedAdvanceFares' in html
|
|
|
|
|
|
def test_results_inbound_uses_reverse_legs(monkeypatch):
|
|
monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None)
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None)
|
|
monkeypatch.setattr(
|
|
app_module.rtt_scraper,
|
|
'fetch_from_paddington',
|
|
lambda travel_date, user_agent, station_crs='BRI': [
|
|
{'depart_paddington': '17:15', 'arrive_destination': '18:55', 'headcode': '1B99'},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.gwr_fares_scraper,
|
|
'fetch',
|
|
lambda station_crs, travel_date, direction='to_paddington': {
|
|
'17:15': {'ticket': 'Off-Peak Single', 'price': 63.60, 'code': 'SVS'}
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.eurostar_scraper,
|
|
'fetch',
|
|
lambda destination, travel_date, direction='outbound': [
|
|
{'depart_destination': '15:12', 'arrive_st_pancras': '16:30',
|
|
'destination': destination, 'train_number': 'ES 9035',
|
|
'price': 49, 'seats': 43, 'plus_price': None, 'plus_seats': None},
|
|
],
|
|
)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10?journey_type=inbound')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert 'Paris Gare du Nord → Bristol Temple Meads' in html
|
|
assert '15:12 → 16:30' in html
|
|
assert '17:15 → 18:55' in html
|
|
assert 'ES 9035' in html
|
|
|
|
|
|
def test_results_return_renders_outbound_and_inbound_tables(monkeypatch):
|
|
monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None)
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None)
|
|
monkeypatch.setattr(
|
|
app_module.rtt_scraper,
|
|
'fetch',
|
|
lambda travel_date, user_agent, station_crs='BRI': [
|
|
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.rtt_scraper,
|
|
'fetch_from_paddington',
|
|
lambda travel_date, user_agent, station_crs='BRI': [
|
|
{'depart_paddington': '17:15', 'arrive_destination': '18:55', 'headcode': '1B99'},
|
|
],
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.gwr_fares_scraper,
|
|
'fetch',
|
|
lambda station_crs, travel_date, direction='to_paddington': {
|
|
'07:00': {'ticket': 'Anytime Day Single', 'price': 138.70, 'code': 'SDS'},
|
|
'17:15': {'ticket': 'Off-Peak Single', 'price': 63.60, 'code': 'SVS'},
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
app_module.eurostar_scraper,
|
|
'fetch_return',
|
|
lambda destination, outbound_date, return_date: {
|
|
'outbound': [
|
|
{'depart_st_pancras': '10:01', 'arrive_destination': '13:34',
|
|
'destination': destination, 'train_number': 'ES 9014',
|
|
'price': 59, 'seats': 42, 'plus_price': None, 'plus_seats': None},
|
|
],
|
|
'inbound': [
|
|
{'depart_destination': '15:12', 'arrive_st_pancras': '16:30',
|
|
'destination': destination, 'train_number': 'ES 9035',
|
|
'price': 49, 'seats': 43, 'plus_price': None, 'plus_seats': None},
|
|
],
|
|
},
|
|
)
|
|
monkeypatch.setattr(
|
|
trip_planner_module.circle_line,
|
|
'upcoming_services',
|
|
lambda earliest_board, count=2, direction='pad_to_kx', preceding=0: (
|
|
[
|
|
(datetime(2026, 4, 10, 9, 10), datetime(2026, 4, 10, 9, 25)),
|
|
(datetime(2026, 4, 10, 9, 15), datetime(2026, 4, 10, 9, 30)),
|
|
]
|
|
if direction == 'pad_to_kx'
|
|
else [
|
|
(datetime(2026, 4, 17, 16, 40), datetime(2026, 4, 17, 16, 55)),
|
|
(datetime(2026, 4, 17, 16, 45), datetime(2026, 4, 17, 17, 0)),
|
|
]
|
|
),
|
|
)
|
|
client = _client()
|
|
|
|
resp = client.get('/results/BRI/paris/2026-04-10/return/2026-04-17')
|
|
html = resp.get_data(as_text=True)
|
|
|
|
assert resp.status_code == 200
|
|
assert 'Outbound: Bristol Temple Meads → Paris Gare du Nord' in html
|
|
assert 'Return: Paris Gare du Nord → Bristol Temple Meads' in html
|
|
assert 'Friday 10 April 2026' in html
|
|
assert 'Friday 17 April 2026' in html
|
|
assert '/results/BRI/paris/2026-04-09/return/2026-04-17' in html
|
|
assert '/results/BRI/paris/2026-04-11/return/2026-04-17' in html
|
|
assert '/results/BRI/paris/2026-04-10/return/2026-04-16' in html
|
|
assert '/results/BRI/paris/2026-04-10/return/2026-04-18' in html
|
|
assert "/results/BRI/paris/2026-04-10/return/2026-04-17" in html
|
|
assert 'journey_type=return' not in html
|
|
assert 'return_date=2026-04-17' not in html
|
|
assert 'Circle 09:10 → KX 09:25' in html
|
|
assert 'next 09:15 → KX 09:30' in html
|
|
assert 'earlier 16:40 → PAD 16:55' in html
|
|
assert 'Circle 16:45 → PAD 17:00' in html
|
|
assert 'title="Tight connection">⚠️</span>' in html
|
|
assert 'ES 9014' in html
|
|
assert 'ES 9035' in html
|
|
|
|
|
|
def test_api_advance_fares_returns_json(monkeypatch):
|
|
monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None)
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None)
|
|
monkeypatch.setattr(
|
|
app_module.gwr_fares_scraper,
|
|
'fetch_advance',
|
|
lambda station_crs, travel_date: {
|
|
'07:00': {
|
|
'advance_std': {'ticket': 'Advance Single', 'price': 45.0, 'code': 'ADV'},
|
|
'advance_1st': {'ticket': '1st Advance', 'price': 65.0, 'code': 'AFA'},
|
|
}
|
|
},
|
|
)
|
|
client = _client()
|
|
|
|
resp = client.get('/api/advance_fares/BRI/2026-04-10')
|
|
data = resp.get_json()
|
|
|
|
assert resp.status_code == 200
|
|
assert '07:00' in data
|
|
assert data['07:00']['advance_std']['price'] == 45.0
|
|
assert data['07:00']['advance_1st']['price'] == 65.0
|
|
|
|
|
|
def test_api_advance_fares_404_for_unknown_station(monkeypatch):
|
|
client = _client()
|
|
resp = client.get('/api/advance_fares/XYZ/2026-04-10')
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_api_advance_fares_returns_error_on_scraper_failure(monkeypatch):
|
|
monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None)
|
|
monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None)
|
|
monkeypatch.setattr(
|
|
app_module.gwr_fares_scraper,
|
|
'fetch_advance',
|
|
lambda s, d: (_ for _ in ()).throw(Exception('network error')),
|
|
)
|
|
client = _client()
|
|
|
|
resp = client.get('/api/advance_fares/BRI/2026-04-10')
|
|
data = resp.get_json()
|
|
|
|
assert resp.status_code == 500
|
|
assert 'error' in data
|