327 lines
11 KiB
Python
327 lines
11 KiB
Python
"""@file test_cli.py
|
|
@brief The command line accepts its options and the entry point wires the use case.
|
|
"""
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from tests.unit.fakes import FakeGym, no_sleep
|
|
from tests.unit.test_booking_entry import make_entry
|
|
from tests.unit.test_models import make_card
|
|
|
|
from snack_and_lift import constants
|
|
from snack_and_lift.cli import main, parse_args
|
|
from snack_and_lift.infrastructure.browser import default_profile_dir
|
|
|
|
DUMMY_EMAIL = "dummy@example.invalid"
|
|
DUMMY_PASSWORD = "dummy-password"
|
|
|
|
|
|
@pytest.fixture
|
|
def credentials_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv(constants.ACCOUNT_EMAIL_ENV, DUMMY_EMAIL)
|
|
monkeypatch.setenv(constants.ACCOUNT_PASSWORD_ENV, DUMMY_PASSWORD)
|
|
|
|
|
|
def test_headless_is_off_by_default() -> None:
|
|
assert parse_args([]).headless is False
|
|
|
|
|
|
def test_headless_flag_is_read() -> None:
|
|
assert parse_args(["--headless"]).headless is True
|
|
|
|
|
|
def test_browser_stays_open_by_default_when_not_headless() -> None:
|
|
assert parse_args([]).keep_open is True
|
|
|
|
|
|
def test_browser_is_closed_by_default_when_headless() -> None:
|
|
assert parse_args(["--headless"]).keep_open is False
|
|
|
|
|
|
def test_keep_open_flag_overrides_headless_default() -> None:
|
|
assert parse_args(["--headless", "--keep-open"]).keep_open is True
|
|
|
|
|
|
def test_profile_dir_defaults_to_working_directory() -> None:
|
|
assert parse_args([]).profile_dir == default_profile_dir()
|
|
|
|
|
|
def test_profile_dir_option_is_read(tmp_path: Path) -> None:
|
|
assert parse_args(["--profile-dir", str(tmp_path)]).profile_dir == tmp_path
|
|
|
|
|
|
def test_screenshot_dir_is_optional() -> None:
|
|
assert parse_args([]).screenshot_dir is None
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_saves_both_screenshots_on_success(tmp_path: Path) -> None:
|
|
gym = FakeGym()
|
|
|
|
code = main(["--screenshot-dir", str(tmp_path)], lambda *_: gym)
|
|
|
|
assert code == 0
|
|
assert gym.logins == [(DUMMY_EMAIL, DUMMY_PASSWORD)]
|
|
assert gym.screenshots == [
|
|
tmp_path / "step-1.png",
|
|
tmp_path / "step-2.png",
|
|
tmp_path / "step-3.png",
|
|
tmp_path / "step-7.png",
|
|
]
|
|
assert [path for _, path in gym.text_screenshots] == [
|
|
tmp_path / "step-4.png",
|
|
tmp_path / "step-5.png",
|
|
tmp_path / "step-6.png",
|
|
tmp_path / "step-8.png",
|
|
tmp_path / "step-9.png",
|
|
]
|
|
assert gym.calls == [
|
|
"open_site",
|
|
"save_screenshot",
|
|
"log_in",
|
|
"save_screenshot",
|
|
"list_class_cards",
|
|
"save_screenshot",
|
|
"save_text_screenshot",
|
|
"save_text_screenshot",
|
|
"save_text_screenshot",
|
|
"save_screenshot",
|
|
"save_text_screenshot",
|
|
"save_text_screenshot",
|
|
]
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_books_the_class_and_draws_the_console_text(tmp_path: Path) -> None:
|
|
gym = FakeGym(cards=[make_card("Tue, Aug 19")])
|
|
|
|
assert main(["--screenshot-dir", str(tmp_path)], lambda *_: gym) == 0
|
|
|
|
assert gym.pressed == ["123"]
|
|
text, _ = gym.text_screenshots[0]
|
|
assert "✓ Logged in" in text
|
|
assert "✓ Successfully booked: Spin Class on Tue, Aug 19" in text
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_books_every_target_class_and_logs_the_report(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
gym = FakeGym(
|
|
cards=[
|
|
make_card("Tomorrow (Thu, Aug 14)", card_id="1"),
|
|
make_card("Fri, Aug 15", card_id="2"),
|
|
make_card("Tue, Aug 19", card_id="3", button_text="Join Waitlist"),
|
|
]
|
|
)
|
|
|
|
with caplog.at_level(logging.INFO):
|
|
assert main([], lambda *_: gym) == 0
|
|
|
|
assert gym.pressed == ["1", "3"]
|
|
assert "New bookings: 1\nNew waitlist entries: 1\nAlready booked/waitlisted: 0" in caplog.text
|
|
assert "Total Tuesday & Thursday 6pm classes: 2" in caplog.text
|
|
assert " • [New Booking] Spin Class on Tomorrow (Thu, Aug 14)" in caplog.text
|
|
assert " • [New Waitlist] Spin Class on Tue, Aug 19" in caplog.text
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_saves_step_3_after_the_first_class_and_the_report_images(tmp_path: Path) -> None:
|
|
gym = FakeGym(
|
|
cards=[make_card("Thu, Aug 14", card_id="1"), make_card("Tue, Aug 19", card_id="2")]
|
|
)
|
|
|
|
assert main(["--screenshot-dir", str(tmp_path)], lambda *_: gym) == 0
|
|
|
|
assert gym.calls == [
|
|
"open_site",
|
|
"save_screenshot",
|
|
"log_in",
|
|
"save_screenshot",
|
|
"list_class_cards",
|
|
"press_class_button",
|
|
"save_screenshot",
|
|
"press_class_button",
|
|
"save_text_screenshot",
|
|
"save_text_screenshot",
|
|
"save_text_screenshot",
|
|
"open_my_bookings",
|
|
"list_my_bookings",
|
|
"save_screenshot",
|
|
"save_text_screenshot",
|
|
"save_text_screenshot",
|
|
]
|
|
assert gym.screenshots[-2:] == [tmp_path / "step-3.png", tmp_path / "step-7.png"]
|
|
(_, _), (step_5, _), (step_6, _), (step_8, step_8_path), (step_9, step_9_path) = (
|
|
gym.text_screenshots
|
|
)
|
|
assert step_8_path == tmp_path / "step-8.png"
|
|
assert step_8.startswith("--- VERIFYING ON MY BOOKINGS PAGE ---")
|
|
assert step_8.endswith("✅ SUCCESS: All bookings verified!")
|
|
assert step_9_path == tmp_path / "step-9.png"
|
|
assert step_9.startswith("✓ Logged in")
|
|
assert step_9.endswith("✅ SUCCESS: All bookings verified!")
|
|
assert step_5.startswith("--- BOOKING SUMMARY ---\nNew bookings: 2")
|
|
assert "--- DETAILED CLASS LIST ---" not in step_5
|
|
assert step_6.startswith(step_5)
|
|
assert "--- DETAILED CLASS LIST ---\n • [New Booking] Spin Class on Thu, Aug 14" in step_6
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_logs_the_verification_and_returns_0_on_success(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
gym = FakeGym(cards=[make_card("Tue, Aug 19")])
|
|
|
|
with caplog.at_level(logging.INFO):
|
|
assert main([], lambda *_: gym) == 0
|
|
|
|
assert "--- VERIFYING ON MY BOOKINGS PAGE ---\n ✓ Verified: Spin Class" in caplog.text
|
|
assert "Expected: 1 booking\nFound: 1 booking\n✅ SUCCESS" in caplog.text
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_notes_bookings_after_the_handled_classes_and_returns_0(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
gym = FakeGym(
|
|
cards=[make_card("Tue, Aug 19")],
|
|
bookings=[make_entry("Tue, Aug 19, 6:00 PM"), make_entry("Tue, Aug 26, 6:00 PM")],
|
|
)
|
|
|
|
with caplog.at_level(logging.INFO):
|
|
assert main([], lambda *_: gym) == 0
|
|
|
|
assert " i Also booked (not checked): Spin Class on Tue, Aug 26\n✅ SUCCESS" in caplog.text
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_returns_3_on_a_mismatch(tmp_path: Path, caplog: pytest.LogCaptureFixture) -> None:
|
|
gym = FakeGym(cards=[make_card("Tue, Aug 19")], bookings=[make_entry("Wed, Aug 20, 7:00 AM")])
|
|
|
|
with caplog.at_level(logging.INFO):
|
|
code = main(["--headless", "--screenshot-dir", str(tmp_path)], lambda *_: gym)
|
|
|
|
assert code == 3
|
|
assert "❌ MISMATCH: Missing 1 bookings" in caplog.text
|
|
assert " ✗ Missing: Spin Class on Tue, Aug 19" in caplog.text
|
|
assert gym.screenshots[-1] == tmp_path / "step-7.png"
|
|
assert gym.text_screenshots[-2][1] == tmp_path / "step-8.png"
|
|
assert gym.text_screenshots[-1][1] == tmp_path / "step-9.png"
|
|
assert gym.closed is True
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_returns_1_when_the_bookings_page_is_empty() -> None:
|
|
gym = FakeGym(cards=[make_card("Tue, Aug 19")], bookings=[])
|
|
|
|
assert main([], lambda *_: gym, no_sleep) == 1
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_logs_an_empty_report_without_target_classes(
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
gym = FakeGym(cards=[make_card("Wed, Aug 13")])
|
|
|
|
with caplog.at_level(logging.INFO):
|
|
assert main([], lambda *_: gym) == 0
|
|
|
|
assert "Total Tuesday & Thursday 6pm classes: 0" in caplog.text
|
|
assert "No matching class found" in caplog.text
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_first_tuesday_only_books_one_class_and_no_report(
|
|
tmp_path: Path, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
gym = FakeGym(
|
|
cards=[make_card("Thu, Aug 14", card_id="1"), make_card("Tue, Aug 19", card_id="2")]
|
|
)
|
|
|
|
with caplog.at_level(logging.INFO):
|
|
code = main(["--first-tuesday-only", "--screenshot-dir", str(tmp_path)], lambda *_: gym)
|
|
|
|
assert code == 0
|
|
assert gym.pressed == ["2"]
|
|
assert "BOOKING SUMMARY" not in caplog.text
|
|
assert "VERIFYING" not in caplog.text
|
|
assert "open_my_bookings" not in gym.calls
|
|
assert [path.name for path in gym.screenshots] == ["step-1.png", "step-2.png", "step-3.png"]
|
|
assert [path.name for _, path in gym.text_screenshots] == ["step-4.png"]
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_first_tuesday_only_flag_is_read() -> None:
|
|
assert parse_args(["--first-tuesday-only"]).first_tuesday_only is True
|
|
assert parse_args([]).first_tuesday_only is False
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_returns_1_when_the_booking_fails() -> None:
|
|
gym = FakeGym(cards=[make_card("Tue, Aug 19")], press_error="Network down")
|
|
|
|
assert main(["--headless"], lambda *_: gym, no_sleep) == 1
|
|
assert gym.closed is True
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_saves_no_screenshot_without_directory() -> None:
|
|
gym = FakeGym()
|
|
|
|
assert main([], lambda *_: gym) == 0
|
|
assert gym.screenshots == []
|
|
|
|
|
|
def test_main_returns_2_when_credentials_are_missing(
|
|
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
monkeypatch.delenv(constants.ACCOUNT_EMAIL_ENV, raising=False)
|
|
monkeypatch.delenv(constants.ACCOUNT_PASSWORD_ENV, raising=False)
|
|
gym = FakeGym()
|
|
|
|
code = main([], lambda *_: gym)
|
|
|
|
assert code == 2
|
|
assert gym.calls == []
|
|
assert constants.ACCOUNT_EMAIL_ENV in caplog.text
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_returns_1_when_login_fails() -> None:
|
|
gym = FakeGym(login_error="Invalid credentials")
|
|
|
|
assert main(["--headless"], lambda *_: gym, no_sleep) == 1
|
|
assert gym.closed is True
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_closes_the_gym_when_not_kept_open() -> None:
|
|
gym = FakeGym()
|
|
|
|
assert main(["--headless"], lambda *_: gym) == 0
|
|
assert gym.closed is True
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_keeps_the_gym_open_when_asked() -> None:
|
|
gym = FakeGym()
|
|
|
|
assert main([], lambda *_: gym) == 0
|
|
assert gym.closed is False
|
|
|
|
|
|
@pytest.mark.usefixtures("credentials_env")
|
|
def test_main_passes_options_to_the_factory(tmp_path: Path) -> None:
|
|
received: list[tuple[bool, Path, bool]] = []
|
|
|
|
def factory(headless: bool, profile_dir: Path, keep_open: bool) -> FakeGym:
|
|
received.append((headless, profile_dir, keep_open))
|
|
return FakeGym()
|
|
|
|
main(["--headless", "--profile-dir", str(tmp_path)], factory)
|
|
|
|
assert received == [(True, tmp_path, False)]
|