Removes || true from the ruff and pytest steps (job names unchanged so branch protection contexts still match), adds [tool.ruff] config (E4/E7/E9/F/I, line-length 120), applies ruff autofixes, and rewrites tests that had drifted from the current calendar/auth/main APIs — failures the soft gates had been hiding.
92 lines
3.1 KiB
Python
92 lines
3.1 KiB
Python
"""Tests for the Airbnb calendar price automation module."""
|
|
|
|
from datetime import date
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from playwright.sync_api import TimeoutError as PlaywrightTimeout
|
|
|
|
from src.airbnb.calendar import (
|
|
_MAX_UPDATE_ATTEMPTS,
|
|
DEFAULT_HOST_ORIGIN,
|
|
parse_month_heading_text,
|
|
resolve_calendar_url,
|
|
update_price,
|
|
)
|
|
|
|
CALENDAR_URL = f"{DEFAULT_HOST_ORIGIN}/multicalendar/12345"
|
|
|
|
|
|
class TestResolveCalendarUrl:
|
|
def test_override_wins(self):
|
|
url = resolve_calendar_url("12345", "https://www.airbnb.ca/multicalendar/999/")
|
|
assert url == "https://www.airbnb.ca/multicalendar/999"
|
|
|
|
def test_listing_id_builds_multicalendar_url(self):
|
|
assert resolve_calendar_url("12345", "") == f"{DEFAULT_HOST_ORIGIN}/multicalendar/12345"
|
|
|
|
def test_no_listing_id_falls_back_to_hosting_calendar(self):
|
|
assert resolve_calendar_url("", "") == f"{DEFAULT_HOST_ORIGIN}/hosting/calendar"
|
|
|
|
|
|
class TestParseMonthHeadingText:
|
|
def test_parses_month_and_year(self):
|
|
assert parse_month_heading_text("April 2026") == (2026, 4)
|
|
|
|
def test_parses_with_surrounding_text(self):
|
|
assert parse_month_heading_text(" December 2025 ") == (2025, 12)
|
|
|
|
def test_rejects_garbage(self):
|
|
assert parse_month_heading_text("not a month heading") is None
|
|
|
|
|
|
class TestUpdatePrice:
|
|
def test_successful_price_update(self):
|
|
page = MagicMock()
|
|
with patch("src.airbnb.calendar._run_price_update_attempt") as attempt:
|
|
result = update_price(page, date(2026, 5, 10), 180, CALENDAR_URL)
|
|
|
|
assert result is True
|
|
attempt.assert_called_once_with(page, date(2026, 5, 10), 180, CALENDAR_URL)
|
|
|
|
def test_retries_on_timeout(self):
|
|
page = MagicMock()
|
|
with (
|
|
patch(
|
|
"src.airbnb.calendar._run_price_update_attempt",
|
|
side_effect=PlaywrightTimeout("timed out"),
|
|
) as attempt,
|
|
patch("src.airbnb.calendar.time.sleep"),
|
|
):
|
|
result = update_price(page, date(2026, 5, 10), 180, CALENDAR_URL)
|
|
|
|
assert result is False
|
|
assert attempt.call_count == _MAX_UPDATE_ATTEMPTS
|
|
|
|
def test_retries_on_generic_exception(self):
|
|
page = MagicMock()
|
|
with (
|
|
patch(
|
|
"src.airbnb.calendar._run_price_update_attempt",
|
|
side_effect=RuntimeError("unexpected"),
|
|
) as attempt,
|
|
patch("src.airbnb.calendar.time.sleep"),
|
|
):
|
|
result = update_price(page, date(2026, 5, 10), 180, CALENDAR_URL)
|
|
|
|
assert result is False
|
|
assert attempt.call_count == _MAX_UPDATE_ATTEMPTS
|
|
|
|
def test_succeeds_on_second_attempt(self):
|
|
page = MagicMock()
|
|
with (
|
|
patch(
|
|
"src.airbnb.calendar._run_price_update_attempt",
|
|
side_effect=[PlaywrightTimeout("first fail"), None],
|
|
) as attempt,
|
|
patch("src.airbnb.calendar.time.sleep"),
|
|
):
|
|
result = update_price(page, date(2026, 5, 10), 180, CALENDAR_URL)
|
|
|
|
assert result is True
|
|
assert attempt.call_count == 2
|