115 lines
5.4 KiB
Python
115 lines
5.4 KiB
Python
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"<root/>"
|
|
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()
|