Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
names,
exports,
entity_history,
update_history,
ancient_pools,
runs,
pairings,
Expand Down Expand Up @@ -660,6 +661,7 @@ async def dispatch(self, request: Request, call_next):
app.include_router(names.router)
app.include_router(exports.router)
app.include_router(entity_history.router)
app.include_router(update_history.router)
app.include_router(ancient_pools.router)
app.include_router(runs.router)
app.include_router(pairings.router)
Expand Down
35 changes: 35 additions & 0 deletions backend/app/routers/update_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Per-entity game-patch update history from data/entity_history.json
(maintained by tools/entity_history_sync.py). English-only."""

import json
from functools import lru_cache

from fastapi import APIRouter, HTTPException

from ..services.data_service import DATA_DIR

router = APIRouter(prefix="/api/update-history", tags=["Update History"])


@lru_cache(maxsize=1)
def _histories() -> dict[str, dict[str, list]]:
try:
with open(DATA_DIR / "entity_history.json", "r", encoding="utf-8") as f:
data = json.load(f)
except (OSError, ValueError):
return {}
types = data.get("types") if isinstance(data, dict) else None
return types if isinstance(types, dict) else {}


@router.get("/{entity_type}/{entity_id}")
def get_update_history(entity_type: str, entity_id: str):
"""Game-patch changes for one entity, newest first. 404s when nothing is
recorded, so the frontend can fall back to the changelog timeline."""
entries = _histories().get(entity_type, {}).get(entity_id.upper())
if not entries:
raise HTTPException(
status_code=404,
detail=f"No update history for '{entity_type}/{entity_id}'",
)
return entries
65 changes: 65 additions & 0 deletions backend/tests/test_update_history.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""The per-entity update-history endpoint serves the game-patch tables in
data/entity_history.json; unknown entities 404 so the frontend can fall
back to the site-changelog timeline."""

import pytest
from fastapi import HTTPException

from app.routers.update_history import _histories, get_update_history


def test_history_file_loads_and_is_well_formed():
types = _histories()
assert len(types["cards"]) > 400
assert len(types["relics"]) > 150
for entity_type, entities in types.items():
for entity_id, entries in list(entities.items())[:30]:
assert entity_id == entity_id.upper(), (entity_type, entity_id)
assert entries
for entry in entries:
assert set(entry) == {"version", "type", "date", "changes"}
assert isinstance(entry["changes"], list)
assert all(isinstance(c, str) and c for c in entry["changes"])


def test_dated_entries_are_newest_first():
checked = 0
for entities in _histories().values():
for entries in entities.values():
dates = [e["date"] for e in entries if e["date"]]
if len(dates) > 1:
assert dates == sorted(dates, reverse=True)
checked += 1
assert checked > 50


def test_known_entity_returns_its_entries():
types = _histories()
card_id = next(iter(types["cards"]))
assert get_update_history("cards", card_id.lower()) == types["cards"][card_id]


def test_unknown_entity_404s():
for entity_type, entity_id in [
("cards", "NOT_A_REAL_CARD"),
("no_such_type", "WHATEVER"),
]:
with pytest.raises(HTTPException) as exc:
get_update_history(entity_type, entity_id)
assert exc.value.status_code == 404


def test_starter_variants_have_history():
# Duplicate-named starters resolve to per-character pages
# (e.g. "Strike (Ironclad)"), which regressed once already.
cards = _histories()["cards"]
assert "STRIKE_IRONCLAD" in cards
assert "DEFEND_IRONCLAD" in cards


def test_name_collisions_stay_in_their_type():
types = _histories()
# Accelerant is both a card and a power; the power page lookup must not
# pick up the card page's history.
assert "ACCELERANT" in types["cards"]
assert "ACCELERANT" not in types.get("powers", {})
Loading
Loading