from pathlib import Path import asyncio from datetime import datetime import json import subprocess from types import SimpleNamespace import pytest from sqlalchemy import select from hockey_data.config import HockeySettingsStore from hockey_data.models import Game, GameRoster, Tournament from hockey_data.service import HockeyDataService from tests.support import LocalTestDatabase @pytest.mark.parametrize("case", [ "first_selection_unchanged_live_hydrates", "finished_empty_loaded_cache_repairs_once", "finished_complete_cache_remains_offline", "unchanged_newer_details_preserve_new_score", "unchanged_incomplete_live_preserves_loaded_roster", "restore_empty_finished_cache_repairs", "late_unchanged_roster_cannot_cross_selection", ]) def test_roster_first_selection_frontend(case): root = Path(__file__).resolve().parents[1] result = subprocess.run( ["node", "tests/roster_first_selection.cjs", case], cwd=root, capture_output=True, text=True, timeout=15, ) assert result.returncode == 0, result.stdout + result.stderr assert f"PASS first roster selection: {case}" in result.stdout @pytest.mark.parametrize("missing_side", ["home", "away", "both", "none", "empty_source"]) def test_unchanged_live_card_repairs_actual_missing_sql_roster(tmp_path, monkeypatch, missing_side): database = LocalTestDatabase(tmp_path / "roster.sqlite") database.create_all() settings = HockeySettingsStore(tmp_path / "settings") settings.update_public({ "match_json_endpoint_template": "match-ru-{game_id}.json", "match_json_en_endpoint_template": "match-en-{game_id}.json", "players_endpoint_template": "players.xml", "player_countries_endpoint_template": "countries.xml", "referees_endpoint_template": "referees.xml", }) raw = json.dumps({ "game": {"idschedule": 900001, "teamA": "Home", "teamB": "Away", "idclubA": 1, "idteamA": 11, "idclubB": 2, "idteamB": 22}, "players": {} if missing_side == "empty_source" else { "A": {"17": {"id": "101", "name": "Home Player", "ps": "f", "pig": 1}}, "B": {"30": {"id": "202", "name": "Away Player", "ps": "g", "pig": 1}}, }, }) service = HockeyDataService(base_dir=tmp_path, database=database, settings=settings) with database.session() as session: session.add(Tournament(external_id="t", season_part="regular", level="khl")) session.add(Game(external_id="900001", tournament_external_id="t", home_team_external_id="1", away_team_external_id="2", details_raw_payload_ru=raw, details_raw_payload_en=raw, details_synced_at=datetime(2026, 10, 22))) requests = [] async def local_resource(endpoint, **kwargs): requests.append(endpoint) content = raw.encode() if endpoint.startswith("match-") else b"" return SimpleNamespace(content=content, auth_mode="fixture") async def local_schedule(**kwargs): return service.game("900001") async def local_shots(**kwargs): return {"available": False} monkeypatch.setattr(service, "_fetch_cached_resource", local_resource) monkeypatch.setattr(service, "sync_selected_game", local_schedule) monkeypatch.setattr(service, "_sync_optional_shots", local_shots) async def scenario(): # Establish a real parsed/persisted card, then simulate an incomplete # local hydration independently of the unchanged upstream JSON. await service.sync_match_details(tournament_external_id="t", game_external_id="900001") with database.session() as session: for row in session.scalars(select(GameRoster).where(GameRoster.game_external_id == "900001")): if missing_side == "both" or row.side == missing_side: session.delete(row) before = service.game_details("900001") assert before is not None and before["details"]["loaded"] is True actual_before = sum(len(before["game"][side]["players"]) for side in ("home", "away")) assert actual_before == (2 if missing_side == "none" else 0 if missing_side in {"both", "empty_source"} else 1) requests.clear() try: result = await service.sync_live_game(tournament_external_id="t", game_external_id="900001") if missing_side == "empty_source": assert result["game"]["home"]["players"] == [] assert result["game"]["away"]["players"] == [] assert result["changed"] is False assert requests == ["match-ru-900001.json"] return assert result["game"]["home"]["players"][0]["id"] == "101" assert result["game"]["away"]["players"][0]["id"] == "202" assert result["changed"] is (missing_side != "none"), "Roster hydration must reach already-open browsers" if missing_side == "none": assert requests == ["match-ru-900001.json"], "A complete unchanged roster was imported again" with database.session() as session: rows = list(session.scalars(select(GameRoster).where(GameRoster.game_external_id == "900001"))) assert {(row.side, row.player_external_id) for row in rows} == {("home", "101"), ("away", "202")} finally: await service.stop_live_refreshes() try: asyncio.run(scenario()) finally: database.engine.dispose()