bristol-eurostar/tests/test_app.py
Edward Betts 6b044b9493 Add 24-hour TTL to Eurostar price cache
Cache reads now accept an optional ttl (seconds). get_cached checks the
file mtime and returns None if the entry is older than the TTL, triggering
a fresh fetch. Eurostar prices use a 24-hour TTL; timetable caches remain
indefinite (date-scoped keys become irrelevant once the date passes).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 10:38:47 +01:00

272 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import app as app_module
def _client():
app_module.app.config['TESTING'] = True
return app_module.app.test_client()
def _stub_data(monkeypatch, prices=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: [
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
],
)
monkeypatch.setattr(
app_module.eurostar_scraper,
'fetch',
lambda destination, travel_date, user_agent: [
{
'depart_st_pancras': '10:01',
'arrive_destination': '13:34',
'destination': destination,
'train_number': 'ES 9014',
},
],
)
monkeypatch.setattr(
app_module.eurostar_scraper,
'timetable_url',
lambda destination: f'https://example.test/{destination.lower().replace(" ", "-")}',
)
_prices = prices if prices is not None else {}
monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: _prices)
def test_index_shows_fixed_departure_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 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')
assert resp.status_code == 302
assert resp.headers['Location'].endswith(
'/results/rotterdam/2026-04-10?min_connection=60&max_connection=120'
)
def test_results_shows_same_day_destination_switcher(monkeypatch):
_stub_data(monkeypatch)
client = _client()
resp = client.get('/results/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/brussels/2026-04-10?min_connection=60&amp;max_connection=120' in html
assert '/results/rotterdam/2026-04-10?min_connection=60&amp;max_connection=120' in html
assert 'ES 9014' in html
def test_results_title_and_social_meta_include_destination(monkeypatch):
_stub_data(monkeypatch)
client = _client()
resp = client.get('/results/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 to Lille Europe via Eurostar</title>' in html
assert '<meta property="og:title" content="Bristol 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/lille/2026-04-10?min_connection=60&amp;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, 'fetch_eurostar_prices', lambda dest, date: {})
monkeypatch.setattr(
app_module.rtt_scraper,
'fetch',
lambda travel_date, user_agent: [
{'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, user_agent: [
{
'depart_st_pancras': '09:30',
'arrive_destination': '11:50',
'destination': destination,
'train_number': 'ES 1001',
},
{
'depart_st_pancras': '09:40',
'arrive_destination': '12:00',
'destination': destination,
'train_number': 'ES 1002',
},
{
'depart_st_pancras': '09:50',
'arrive_destination': '12:20',
'destination': destination,
'train_number': 'ES 1003',
},
{
'depart_st_pancras': '10:00',
'arrive_destination': '12:35',
'destination': destination,
'train_number': 'ES 1004',
},
{
'depart_st_pancras': '10:10',
'arrive_destination': '12:45',
'destination': destination,
'train_number': 'ES 1005',
},
],
)
monkeypatch.setattr(
app_module.eurostar_scraper,
'timetable_url',
lambda destination: f'https://example.test/{destination.lower().replace(" ", "-")}',
)
client = _client()
resp = client.get('/results/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 option"') == 2
assert html.count('title="Slowest option"') == 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_unreachable_morning_eurostar_services(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, 'fetch_eurostar_prices', lambda dest, date: {})
monkeypatch.setattr(
app_module.rtt_scraper,
'fetch',
lambda travel_date, user_agent: [
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
],
)
monkeypatch.setattr(
app_module.eurostar_scraper,
'fetch',
lambda destination, travel_date, user_agent: [
{
'depart_st_pancras': '09:30',
'arrive_destination': '12:00',
'destination': destination,
'train_number': 'ES 9001',
},
{
'depart_st_pancras': '10:15',
'arrive_destination': '13:40',
'destination': destination,
'train_number': 'ES 9002',
},
{
'depart_st_pancras': '12:30',
'arrive_destination': '15:55',
'destination': destination,
'train_number': 'ES 9003',
},
],
)
monkeypatch.setattr(
app_module.eurostar_scraper,
'timetable_url',
lambda destination: f'https://example.test/{destination.lower().replace(" ", "-")}',
)
client = _client()
resp = client.get('/results/paris/2026-04-10?min_connection=60&max_connection=120')
html = resp.get_data(as_text=True)
assert resp.status_code == 200
assert '1 morning service unavailable from Bristol' in html
assert '09:30' in html
assert 'ES 9001' in html
assert 'Unavailable from Bristol' in html
assert 'Morning means Eurostar departures before 12:00 from St&nbsp;Pancras.' in html
assert html.index('09:30') < html.index('10:15')
def test_results_shows_eurostar_price_and_total(monkeypatch):
# 07:00 on Friday 2026-04-10 → Anytime £138.70 (weekday, 05:0508:25 window)
_stub_data(monkeypatch, prices={'10:01': 59})
client = _client()
resp = client.get('/results/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 '£197.70' in html # Anytime £138.70 + ES £59
def test_results_can_show_only_unreachable_morning_services(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, 'fetch_eurostar_prices', lambda dest, date: {})
monkeypatch.setattr(
app_module.rtt_scraper,
'fetch',
lambda travel_date, user_agent: [
{'depart_bristol': '07:00', 'arrive_paddington': '08:45', 'headcode': '1A23'},
],
)
monkeypatch.setattr(
app_module.eurostar_scraper,
'fetch',
lambda destination, travel_date, user_agent: [
{
'depart_st_pancras': '09:30',
'arrive_destination': '12:00',
'destination': destination,
'train_number': 'ES 9001',
},
],
)
monkeypatch.setattr(
app_module.eurostar_scraper,
'timetable_url',
lambda destination: f'https://example.test/{destination.lower().replace(" ", "-")}',
)
client = _client()
resp = client.get('/results/paris/2026-04-10?min_connection=60&max_connection=120')
html = resp.get_data(as_text=True)
assert resp.status_code == 200
assert 'No valid journeys found.' not in html
assert '1 morning service unavailable from Bristol' in html
assert '09:30' in html
assert 'Unavailable from Bristol' in html