Files
NHL-Scoreboard/tests/test_routes.py
T
josh 930247b32f
CI / Lint (push) Successful in 8s
CI / Test (push) Successful in 10s
CI / Build & Push (push) Successful in 22s
style: apply ruff format and fix lint issues in playoff modules
- Rename single-letter `l` loop variables in bracket_view to satisfy E741
- Drop unused `json` import from test_playoff_cache (F401/F811)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 12:48:42 -04:00

318 lines
11 KiB
Python

import json
from tests.conftest import make_game, make_playoff_game
class TestIndexRoute:
def test_returns_200(self, flask_client):
response = flask_client.get("/")
assert response.status_code == 200
def test_returns_html(self, flask_client):
response = flask_client.get("/")
assert b"NHL Scoreboard" in response.data
class TestScoreboardRoute:
def test_returns_200(self, flask_client):
response = flask_client.get("/scoreboard")
assert response.status_code == 200
def test_returns_json_with_expected_keys(self, flask_client):
response = flask_client.get("/scoreboard")
data = json.loads(response.data)
assert "live_games" in data
assert "intermission_games" in data
assert "pre_games" in data
assert "final_games" in data
def test_intermission_games_separated_from_live(
self, flask_client, monkeypatch, tmp_path
):
import json as _json
import app.routes as routes
intermission_game = make_game(in_intermission=True)
live_game = make_game(home_name="Oilers", away_name="Flames")
scoreboard = {"games": [intermission_game, live_game]}
f = tmp_path / "scoreboard_data.json"
f.write_text(_json.dumps(scoreboard))
monkeypatch.setattr(routes, "SCOREBOARD_DATA_FILE", str(f))
data = _json.loads(flask_client.get("/scoreboard").data)
assert len(data["intermission_games"]) == 1
assert data["intermission_games"][0]["Intermission"] is True
assert len(data["live_games"]) == 1
assert data["live_games"][0]["Intermission"] is False
def test_live_games_have_required_fields(self, flask_client):
response = flask_client.get("/scoreboard")
data = json.loads(response.data)
if data["live_games"]:
game = data["live_games"][0]
assert "Home Team" in game
assert "Away Team" in game
assert "Home Score" in game
assert "Away Score" in game
assert "Game State" in game
assert game["Game State"] == "LIVE"
def test_missing_file_returns_error(self, flask_client, monkeypatch):
import app.routes as routes
monkeypatch.setattr(routes, "SCOREBOARD_DATA_FILE", "/nonexistent/path.json")
response = flask_client.get("/scoreboard")
data = json.loads(response.data)
assert "error" in data
def test_invalid_json_returns_error(self, flask_client, monkeypatch, tmp_path):
import app.routes as routes
bad_file = tmp_path / "bad.json"
bad_file.write_text("not valid json {{{")
monkeypatch.setattr(routes, "SCOREBOARD_DATA_FILE", str(bad_file))
response = flask_client.get("/scoreboard")
data = json.loads(response.data)
assert "error" in data
def test_null_json_returns_error(self, flask_client, monkeypatch, tmp_path):
import app.routes as routes
null_file = tmp_path / "null.json"
null_file.write_text("null")
monkeypatch.setattr(routes, "SCOREBOARD_DATA_FILE", str(null_file))
response = flask_client.get("/scoreboard")
data = json.loads(response.data)
assert "error" in data
def test_meta_and_pinned_keys_present(self, flask_client):
response = flask_client.get("/scoreboard")
data = json.loads(response.data)
assert "meta" in data
assert "pinned_games" in data
assert "playoff_mode" in data["meta"]
def test_meta_playoff_mode_off_for_regular_season(self, flask_client):
response = flask_client.get("/scoreboard")
data = json.loads(response.data)
assert data["meta"]["playoff_mode"] is False
def test_playoff_day_populates_meta(self, flask_client, monkeypatch, tmp_path):
import app.routes as routes
playoff_game = make_playoff_game(
top_wins=3,
bottom_wins=3,
round_num=1,
series_letter="A",
game_state="LIVE",
)
scoreboard = {"games": [playoff_game]}
f = tmp_path / "scoreboard_data.json"
f.write_text(json.dumps(scoreboard))
monkeypatch.setattr(routes, "SCOREBOARD_DATA_FILE", str(f))
data = json.loads(flask_client.get("/scoreboard").data)
assert data["meta"]["playoff_mode"] is True
assert data["meta"]["round_label"] == "First Round"
assert data["meta"]["game7_count"] == 1
assert data["meta"]["series_active"] == 1
def test_game7_goes_to_pinned_bucket_not_live(
self, flask_client, monkeypatch, tmp_path
):
import app.routes as routes
g7 = make_playoff_game(
top_wins=3,
bottom_wins=3,
game_state="LIVE",
home_name="Kings",
away_name="Oilers",
)
regular_live = make_game(home_name="Rangers", away_name="Devils")
scoreboard = {"games": [g7, regular_live]}
f = tmp_path / "scoreboard_data.json"
f.write_text(json.dumps(scoreboard))
monkeypatch.setattr(routes, "SCOREBOARD_DATA_FILE", str(f))
data = json.loads(flask_client.get("/scoreboard").data)
pinned_names = [g["Home Team"] for g in data["pinned_games"]]
live_names = [g["Home Team"] for g in data["live_games"]]
assert "Kings" in pinned_names
assert "Kings" not in live_names
assert "Rangers" in live_names
class TestSeriesDetailRoute:
_SAMPLE_PAYLOAD = {
"round": 1,
"roundLabel": "1st-round",
"seriesLetter": "A",
"neededToWin": 4,
"length": 7,
"topSeedTeam": {
"id": 10,
"name": {"default": "Maple Leafs"},
"abbrev": "TOR",
"placeName": {"default": "Toronto"},
"record": "2-1",
"seriesWins": 2,
"divisionAbbrev": "A",
"seed": 1,
"logo": "https://example.com/tor.svg",
"darkLogo": "https://example.com/tor_dark.svg",
"conference": {"abbrev": "E"},
},
"bottomSeedTeam": {
"id": 9,
"name": {"default": "Senators"},
"abbrev": "OTT",
"placeName": {"default": "Ottawa"},
"record": "1-2",
"seriesWins": 1,
"divisionAbbrev": "A",
"seed": 4,
"logo": "https://example.com/ott.svg",
"darkLogo": "https://example.com/ott_dark.svg",
"conference": {"abbrev": "E"},
},
"games": [
{
"id": 1,
"gameNumber": 1,
"ifNecessary": False,
"venue": {"default": "Arena"},
"startTimeUTC": "2026-04-18T23:00:00Z",
"gameState": "OFF",
"periodDescriptor": {"number": 3, "periodType": "REG"},
"awayTeam": {
"abbrev": "OTT",
"score": 2,
"commonName": {"default": "Senators"},
},
"homeTeam": {
"abbrev": "TOR",
"score": 6,
"commonName": {"default": "Maple Leafs"},
},
"gameOutcome": {"lastPeriodType": "REG"},
},
{
"id": 2,
"gameNumber": 4,
"ifNecessary": False,
"venue": {"default": "Arena"},
"startTimeUTC": "2026-04-22T23:00:00Z",
"gameState": "FUT",
"periodDescriptor": {"number": 1, "periodType": "REG"},
"awayTeam": {"abbrev": "TOR", "commonName": {"default": "Maple Leafs"}},
"homeTeam": {"abbrev": "OTT", "commonName": {"default": "Senators"}},
},
],
}
def test_invalid_series_id_404(self, flask_client):
response = flask_client.get("/series/garbage")
assert response.status_code == 404
def test_valid_format_but_missing_cache_404(self, flask_client, monkeypatch):
import app.routes as routes
monkeypatch.setattr(routes, "fetch_series", lambda sid: None)
response = flask_client.get("/series/2026-A")
assert response.status_code == 404
def test_renders_with_cached_payload(self, flask_client, monkeypatch):
import app.routes as routes
monkeypatch.setattr(routes, "fetch_series", lambda sid: self._SAMPLE_PAYLOAD)
response = flask_client.get("/series/2026-A")
assert response.status_code == 200
body = response.data.decode("utf-8")
assert "Maple Leafs" in body
assert "Senators" in body
assert "Game 1" in body
assert "Game 4" in body
def test_letter_out_of_range_404(self, flask_client):
response = flask_client.get("/series/2026-Z")
assert response.status_code == 404
class TestBracketRoute:
_BRACKET = {
"bracketLogo": "http://example.com/bracket.png",
"series": [
{
"seriesLetter": "A",
"playoffRound": 1,
"topSeedWins": 2,
"bottomSeedWins": 1,
"topSeedRankAbbrev": "D1",
"bottomSeedRankAbbrev": "WC1",
"winningTeamId": None,
"topSeedTeam": {
"id": 10,
"abbrev": "TOR",
"name": {"default": "Toronto Maple Leafs"},
"commonName": {"default": "Maple Leafs"},
"darkLogo": "http://example.com/TOR.svg",
},
"bottomSeedTeam": {
"id": 9,
"abbrev": "OTT",
"name": {"default": "Ottawa Senators"},
"commonName": {"default": "Senators"},
"darkLogo": "http://example.com/OTT.svg",
},
}
],
}
def test_bracket_renders_with_cache(self, flask_client, monkeypatch):
import app.routes as routes
monkeypatch.setattr(routes, "get_bracket", lambda y: (self._BRACKET, 123))
monkeypatch.setattr(
routes,
"refresh_bracket",
lambda y=None: (_ for _ in ()).throw(AssertionError("should not refetch")),
)
response = flask_client.get("/bracket")
assert response.status_code == 200
body = response.data.decode("utf-8")
assert "Stanley Cup Playoffs" in body
assert "TOR" in body
assert "OTT" in body
def test_bracket_falls_back_to_fresh_fetch_when_cache_empty(
self, flask_client, monkeypatch
):
import app.routes as routes
monkeypatch.setattr(routes, "get_bracket", lambda y: (None, None))
called = {"n": 0}
def fake_refresh(year=None):
called["n"] += 1
return self._BRACKET
monkeypatch.setattr(routes, "refresh_bracket", fake_refresh)
response = flask_client.get("/bracket")
assert response.status_code == 200
assert called["n"] == 1
def test_bracket_returns_404_when_no_data(self, flask_client, monkeypatch):
import app.routes as routes
monkeypatch.setattr(routes, "get_bracket", lambda y: (None, None))
monkeypatch.setattr(routes, "refresh_bracket", lambda year=None: None)
response = flask_client.get("/bracket")
assert response.status_code == 404