AtAnyRate/tests/test_main.py
ilia c8a82e264c Add tests, geo search, noise filtering, sports scoring, and dedup improvements.
Tests cover providers, dedup, Telegram, scoring, main runner, and Airbnb stubs.
Ticketmaster and SeatGeek use configurable lat/lon/radius (Thornhill default).
Pipeline filters noise listings, merges same-day sports duplicates, optional
MIN_ALERT_SCORE, and Telegram severity summary.

Made-with: Cursor
2026-04-04 15:25:35 -04:00

319 lines
11 KiB
Python

"""Tests for the main orchestration runner."""
from datetime import date
from unittest.mock import patch, MagicMock
import pytest
from src.main import (
parse_args,
fetch_all_events,
filter_noise,
filter_by_min_score,
print_summary,
update_airbnb_prices,
main,
)
from src.models import NormalizedEvent
def _make_event(name: str, event_date: date, venue: str, score: float = 0.5) -> NormalizedEvent:
return NormalizedEvent(
name=name, event_date=event_date, venue=venue,
source="test", url="https://example.com", score=score,
)
class TestParseArgs:
def test_default_mode(self):
with patch("sys.argv", ["main"]):
args = parse_args()
assert args.dry_run is False
assert args.alerts_only is False
def test_dry_run(self):
with patch("sys.argv", ["main", "--dry-run"]):
args = parse_args()
assert args.dry_run is True
def test_alerts_only(self):
with patch("sys.argv", ["main", "--alerts-only"]):
args = parse_args()
assert args.alerts_only is True
class TestFetchAllEvents:
def test_collects_from_all_providers(self):
mock_settings = MagicMock()
mock_settings.ticketmaster_key = "key"
mock_settings.seatgeek_client_id = "id"
mock_settings.lookahead_days = 30
events = [_make_event("A", date(2026, 5, 10), "V")]
with patch("src.main.TicketmasterProvider") as MockTM, \
patch("src.main.SeatGeekProvider") as MockSG:
MockTM.return_value.name = "ticketmaster"
MockTM.return_value.fetch.return_value = events
MockSG.return_value.name = "seatgeek"
MockSG.return_value.fetch.return_value = events
result = fetch_all_events(mock_settings)
assert len(result) == 2
def test_handles_provider_returning_empty(self):
mock_settings = MagicMock()
mock_settings.ticketmaster_key = "key"
mock_settings.seatgeek_client_id = "id"
mock_settings.lookahead_days = 30
with patch("src.main.TicketmasterProvider") as MockTM, \
patch("src.main.SeatGeekProvider") as MockSG:
MockTM.return_value.name = "ticketmaster"
MockTM.return_value.fetch.return_value = [
_make_event("A", date(2026, 5, 10), "V")
]
MockSG.return_value.name = "seatgeek"
MockSG.return_value.fetch.return_value = []
result = fetch_all_events(mock_settings)
assert len(result) == 1
class TestFilterNoise:
def test_removes_meeting_spaces(self):
events = [
_make_event("Meeting Spaces", date(2026, 5, 10), "Rogers Centre"),
_make_event("Blue Jays vs Yankees", date(2026, 5, 10), "Rogers Centre"),
]
result = filter_noise(events)
assert len(result) == 1
assert result[0].name == "Blue Jays vs Yankees"
def test_removes_ballpark_tours(self):
events = [
_make_event("Rogers Centre Ballpark Tours", date(2026, 5, 10), "Rogers Centre"),
_make_event("Raptors vs Heat", date(2026, 5, 10), "Scotiabank Arena"),
]
result = filter_noise(events)
assert len(result) == 1
def test_removes_guided_tours(self):
events = [
_make_event("Premium Guided Tours of Scotiabank Arena", date(2026, 5, 10), "Scotiabank Arena"),
]
result = filter_noise(events)
assert len(result) == 0
def test_removes_fan_access_addons(self):
events = [
_make_event("Raptors Fan Access Plus Ups: Courtside Access", date(2026, 5, 10), "Scotiabank Arena"),
_make_event("Raptors Fan Access Plus Ups: On-Court Flag Bearer", date(2026, 5, 10), "Scotiabank Arena"),
]
result = filter_noise(events)
assert len(result) == 0
def test_keeps_real_events(self):
events = [
_make_event("Toronto Raptors vs. Miami Heat", date(2026, 5, 10), "Scotiabank Arena"),
_make_event("Drake Concert", date(2026, 5, 15), "Budweiser Stage"),
]
result = filter_noise(events)
assert len(result) == 2
def test_case_insensitive(self):
events = [
_make_event("MEETING SPACES", date(2026, 5, 10), "Rogers Centre"),
]
result = filter_noise(events)
assert len(result) == 0
def test_empty_list(self):
assert filter_noise([]) == []
class TestFilterByMinScore:
def test_zero_passes_all(self):
events = [_make_event("A", date(2026, 5, 10), "V", 0.1)]
assert filter_by_min_score(events, 0.0) == events
def test_filters_low_scores(self):
events = [
_make_event("Big", date(2026, 5, 10), "V", 0.9),
_make_event("Small", date(2026, 5, 11), "V", 0.2),
]
result = filter_by_min_score(events, 0.5)
assert len(result) == 1
assert result[0].name == "Big"
class TestPrintSummary:
def test_prints_no_events_message(self, capsys):
print_summary([])
output = capsys.readouterr().out
assert "No upcoming events" in output
def test_prints_event_details(self, capsys):
events = [_make_event("Raptors Game", date(2026, 5, 10), "Scotiabank Arena", 0.8)]
print_summary(events)
output = capsys.readouterr().out
assert "Raptors Game" in output
assert "Scotiabank Arena" in output
assert "0.80" in output
def test_prints_event_count(self, capsys):
events = [
_make_event("A", date(2026, 5, 10), "V1"),
_make_event("B", date(2026, 5, 11), "V2"),
]
print_summary(events)
output = capsys.readouterr().out
assert "2 events" in output
class TestUpdateAirbnbPrices:
def test_skips_when_no_listing_configured(self):
settings = MagicMock()
settings.airbnb_listing_id = ""
events = [_make_event("A", date(2026, 5, 10), "V")]
update_airbnb_prices(events, settings)
def test_calculates_price_correctly(self):
settings = MagicMock()
settings.airbnb_listing_id = "12345"
settings.airbnb_base_price = 100
settings.price_increase_pct = 25
events = [_make_event("A", date(2026, 5, 10), "V")]
with patch("playwright.sync_api.sync_playwright") as mock_pw, \
patch("src.airbnb.auth.load_authenticated_context") as mock_auth, \
patch("src.airbnb.calendar.update_price") as mock_update:
mock_page = MagicMock()
mock_context = MagicMock()
mock_context.new_page.return_value = mock_page
mock_browser = MagicMock()
mock_auth.return_value = mock_context
mock_pw.return_value.__enter__ = MagicMock(
return_value=MagicMock(chromium=MagicMock(launch=MagicMock(return_value=mock_browser)))
)
mock_update.return_value = True
update_airbnb_prices(events, settings)
mock_update.assert_called_once_with(mock_page, date(2026, 5, 10), 125)
def test_handles_missing_state_file(self):
settings = MagicMock()
settings.airbnb_listing_id = "12345"
settings.airbnb_base_price = 100
settings.price_increase_pct = 20
events = [_make_event("A", date(2026, 5, 10), "V")]
with patch("playwright.sync_api.sync_playwright") as mock_pw:
mock_browser = MagicMock()
mock_p = MagicMock()
mock_p.chromium.launch.return_value = mock_browser
mock_pw.return_value.__enter__ = MagicMock(return_value=mock_p)
mock_browser.close = MagicMock()
with patch("src.airbnb.auth.load_authenticated_context", side_effect=FileNotFoundError("no state")):
update_airbnb_prices(events, settings)
class TestMainRunner:
@patch("src.main.parse_args")
@patch("src.main.load_settings")
@patch("src.main.setup_logging")
@patch("src.main.fetch_all_events")
@patch("src.main.filter_noise")
@patch("src.main.deduplicate")
@patch("src.main.score_events")
@patch("src.main.filter_by_window")
@patch("src.main.print_summary")
@patch("src.main.send_alert")
def test_dry_run_skips_alerts_and_updates(
self, mock_alert, mock_print, mock_filter,
mock_score, mock_dedup, mock_noise, mock_fetch, mock_log,
mock_settings, mock_args,
):
mock_args.return_value.dry_run = True
mock_args.return_value.alerts_only = False
mock_settings.return_value.log_level = "INFO"
mock_settings.return_value.lookahead_days = 30
mock_settings.return_value.min_alert_score = 0.0
events = [_make_event("A", date(2026, 5, 10), "V")]
mock_fetch.return_value = events
mock_noise.return_value = events
mock_dedup.return_value = events
mock_score.return_value = events
mock_filter.return_value = events
main()
mock_print.assert_called_once()
mock_alert.assert_not_called()
@patch("src.main.parse_args")
@patch("src.main.load_settings")
@patch("src.main.setup_logging")
@patch("src.main.fetch_all_events")
@patch("src.main.filter_noise")
@patch("src.main.deduplicate")
@patch("src.main.score_events")
@patch("src.main.filter_by_window")
@patch("src.main.print_summary")
@patch("src.main.send_alert")
@patch("src.main.update_airbnb_prices")
def test_alerts_only_sends_telegram_no_airbnb(
self, mock_airbnb, mock_alert, mock_print, mock_filter,
mock_score, mock_dedup, mock_noise, mock_fetch, mock_log,
mock_settings, mock_args,
):
mock_args.return_value.dry_run = False
mock_args.return_value.alerts_only = True
mock_settings.return_value.log_level = "INFO"
mock_settings.return_value.telegram_bot_token = "token"
mock_settings.return_value.telegram_chat_id = "123"
mock_settings.return_value.lookahead_days = 30
mock_settings.return_value.min_alert_score = 0.0
events = [_make_event("A", date(2026, 5, 10), "V")]
mock_fetch.return_value = events
mock_noise.return_value = events
mock_dedup.return_value = events
mock_score.return_value = events
mock_filter.return_value = events
mock_alert.return_value = True
main()
mock_alert.assert_called_once()
mock_airbnb.assert_not_called()
@patch("src.main.parse_args")
@patch("src.main.load_settings")
@patch("src.main.setup_logging")
@patch("src.main.fetch_all_events")
@patch("src.main.send_alert")
def test_no_events_sends_empty_alert(
self, mock_alert, mock_fetch, mock_log,
mock_settings, mock_args,
):
mock_args.return_value.dry_run = False
mock_args.return_value.alerts_only = False
mock_settings.return_value.log_level = "INFO"
mock_settings.return_value.telegram_bot_token = "token"
mock_settings.return_value.telegram_chat_id = "123"
mock_fetch.return_value = []
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 0
mock_alert.assert_called_once_with([], "token", "123")