"""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")