Fix HTML entities in GWR advance ticket dates

This commit is contained in:
Edward Betts 2026-09-25 15:56:16 +01:00
parent e75acc3364
commit 69992bebb7
2 changed files with 25 additions and 5 deletions

View file

@ -3,6 +3,7 @@
import os
import re
from datetime import date, datetime
from html import unescape
from time import time
import httpx
@ -12,6 +13,7 @@ url = "https://www.gwr.com/your-tickets/choosing-your-ticket/advance-tickets"
def parse_date_string(date_str: str) -> date:
"""Parse date string from HTML."""
date_str = " ".join(unescape(date_str).split())
if not date_str[-1].isdigit(): # If the year is missing, use the current year
date_str += f" {date.today().year}"

View file

@ -2,8 +2,8 @@
import os
import tempfile
import typing
from datetime import date
from typing import Any
from unittest.mock import AsyncMock, patch
import pytest
@ -20,6 +20,16 @@ from agenda.gwr import (
class TestParseDateString:
"""Tests for parse_date_string function."""
@pytest.mark.parametrize("space", [" ", " ", " ", "\u00a0"])
@pytest.mark.parametrize("include_year", [True, False])
def test_parse_html_whitespace(self, space: str, include_year: bool) -> None:
"""Decode HTML spaces before parsing dates or inferring the year."""
date_str = f"Thursday 25{space}March"
if include_year:
date_str += " 2027"
result = parse_date_string(f" {space}{date_str}{space} ")
assert result == date(2027 if include_year else date.today().year, 3, 25)
def test_parse_date_with_year(self) -> None:
"""Test parsing date string with year included."""
result = parse_date_string("Monday 25 December 2023")
@ -152,7 +162,7 @@ class TestAdvanceTicketsPageHtml:
@pytest.mark.asyncio
@patch("httpx.AsyncClient")
async def test_fetch_from_web(self, mock_client: Any) -> None:
async def test_fetch_from_web(self, mock_client: typing.Any) -> None:
"""Test fetching from web when cache is stale."""
mock_response = AsyncMock()
mock_response.text = "<html>fresh content</html>"
@ -176,7 +186,7 @@ class TestAdvanceTicketDate:
@pytest.mark.asyncio
@patch("agenda.gwr.advance_tickets_page_html")
async def test_advance_ticket_date_success(self, mock_html: Any) -> None:
async def test_advance_ticket_date_success(self, mock_html: typing.Any) -> None:
"""Test successfully extracting advance ticket date."""
mock_html.return_value = """
<tr><td>Weekdays</td><td>Monday 25 December 2023</td></tr>
@ -188,7 +198,7 @@ class TestAdvanceTicketDate:
@pytest.mark.asyncio
@patch("agenda.gwr.advance_tickets_page_html")
async def test_advance_ticket_date_no_match(self, mock_html: Any) -> None:
async def test_advance_ticket_date_no_match(self, mock_html: typing.Any) -> None:
"""Test when no weekday date can be extracted."""
mock_html.return_value = "<p>No relevant data</p>"
@ -197,7 +207,7 @@ class TestAdvanceTicketDate:
@pytest.mark.asyncio
@patch("agenda.gwr.advance_tickets_page_html")
async def test_advance_ticket_date_force_cache(self, mock_html: Any) -> None:
async def test_advance_ticket_date_force_cache(self, mock_html: typing.Any) -> None:
"""Test advance_ticket_date with force_cache parameter."""
mock_html.return_value = """
<tr><td>Weekdays</td><td>Tuesday 26 December 2023</td></tr>
@ -206,3 +216,11 @@ class TestAdvanceTicketDate:
result = await advance_ticket_date("/fake/dir", force_cache=True)
assert result == date(2023, 12, 26)
mock_html.assert_called_once_with("/fake/dir", force_cache=True)
@pytest.mark.parametrize("suffix", ["", "**"])
def test_extract_html_entity_date(suffix: str) -> None:
"""Both extraction paths accept the date that caused the update crash."""
html = f"<tr><td>Weekdays</td><td>Thursday 25&nbsp;March 2027{suffix}</td></tr>"
assert extract_dates(html) == {"Weekdays": date(2027, 3, 25)}
assert extract_weekday_date(html) == date(2027, 3, 25)