diff --git a/app/seed/__init__.py b/app/seed/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/seed/evolution_tracks.json b/app/seed/evolution_tracks.json new file mode 100644 index 0000000..a4bd1f0 --- /dev/null +++ b/app/seed/evolution_tracks.json @@ -0,0 +1,5 @@ +[ + {"name": "Batter", "card_type": "batter", "formula": "pa+tb*2", "t1": 37, "t2": 149, "t3": 448, "t4": 896}, + {"name": "Starting Pitcher", "card_type": "sp", "formula": "ip+k", "t1": 10, "t2": 40, "t3": 120, "t4": 240}, + {"name": "Relief Pitcher", "card_type": "rp", "formula": "ip+k", "t1": 3, "t2": 12, "t3": 35, "t4": 70} +] diff --git a/app/seed/evolution_tracks.py b/app/seed/evolution_tracks.py new file mode 100644 index 0000000..178f68e --- /dev/null +++ b/app/seed/evolution_tracks.py @@ -0,0 +1,41 @@ +"""Seed data fixture for EvolutionTrack. + +Inserts the three universal evolution tracks (Batter, Starting Pitcher, +Relief Pitcher) if they do not already exist. Safe to call multiple times +thanks to get_or_create — depends on WP-01 (EvolutionTrack model) to run. +""" + +import json +import os + +_JSON_PATH = os.path.join(os.path.dirname(__file__), "evolution_tracks.json") + + +def load_tracks(): + """Return the locked list of evolution track dicts from the JSON fixture.""" + with open(_JSON_PATH) as fh: + return json.load(fh) + + +def seed(model_class=None): + """Insert evolution tracks that are not yet in the database. + + Args: + model_class: Peewee model with get_or_create support. Defaults to + ``app.db_engine.EvolutionTrack`` (imported lazily so this module + can be imported before WP-01 lands). + + Returns: + List of (instance, created) tuples from get_or_create. + """ + if model_class is None: + from app.db_engine import EvolutionTrack as model_class # noqa: PLC0415 + + results = [] + for track in load_tracks(): + instance, created = model_class.get_or_create( + card_type=track["card_type"], + defaults=track, + ) + results.append((instance, created)) + return results diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_evolution_seed.py b/tests/test_evolution_seed.py new file mode 100644 index 0000000..8aed49c --- /dev/null +++ b/tests/test_evolution_seed.py @@ -0,0 +1,119 @@ +"""Tests for the evolution track seed data fixture (WP-03). + +Unit tests verify the JSON fixture is correctly formed without touching any +database. The integration test binds a minimal in-memory EvolutionTrack +model (mirroring the schema WP-01 will add to db_engine) to an in-memory +SQLite database, calls seed(), and verifies idempotency. +""" + +import pytest +from peewee import CharField, IntegerField, Model, SqliteDatabase + +from app.seed.evolution_tracks import load_tracks, seed + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +_test_db = SqliteDatabase(":memory:") + + +class EvolutionTrackStub(Model): + """Minimal EvolutionTrack model for integration tests. + + Mirrors the schema that WP-01 will add to db_engine so the integration + test can run without WP-01 being merged. + """ + + name = CharField() + card_type = CharField(unique=True) + formula = CharField() + t1 = IntegerField() + t2 = IntegerField() + t3 = IntegerField() + t4 = IntegerField() + + class Meta: + database = _test_db + table_name = "evolution_track" + + +@pytest.fixture(autouse=True) +def _db(): + """Bind and create the stub table; drop it after each test.""" + _test_db.connect(reuse_if_open=True) + _test_db.create_tables([EvolutionTrackStub]) + yield + _test_db.drop_tables([EvolutionTrackStub]) + + +# --------------------------------------------------------------------------- +# Unit tests — JSON fixture only, no database +# --------------------------------------------------------------------------- + + +def test_three_tracks_in_seed_data(): + """load_tracks() must return exactly 3 evolution tracks.""" + assert len(load_tracks()) == 3 + + +def test_card_types_are_exactly_batter_sp_rp(): + """The set of card_type values must be exactly {'batter', 'sp', 'rp'}.""" + types = {t["card_type"] for t in load_tracks()} + assert types == {"batter", "sp", "rp"} + + +def test_all_thresholds_positive_and_ascending(): + """Each track must have t1 < t2 < t3 < t4, all positive.""" + for track in load_tracks(): + assert track["t1"] > 0 + assert track["t1"] < track["t2"] < track["t3"] < track["t4"] + + +def test_all_tracks_have_non_empty_formula(): + """Every track must have a non-empty formula string.""" + for track in load_tracks(): + assert isinstance(track["formula"], str) and track["formula"].strip() + + +def test_tier_thresholds_match_locked_values(): + """Threshold values must exactly match the locked design spec.""" + tracks = {t["card_type"]: t for t in load_tracks()} + + assert tracks["batter"]["t1"] == 37 + assert tracks["batter"]["t2"] == 149 + assert tracks["batter"]["t3"] == 448 + assert tracks["batter"]["t4"] == 896 + + assert tracks["sp"]["t1"] == 10 + assert tracks["sp"]["t2"] == 40 + assert tracks["sp"]["t3"] == 120 + assert tracks["sp"]["t4"] == 240 + + assert tracks["rp"]["t1"] == 3 + assert tracks["rp"]["t2"] == 12 + assert tracks["rp"]["t3"] == 35 + assert tracks["rp"]["t4"] == 70 + + +# --------------------------------------------------------------------------- +# Integration test — uses the stub model + in-memory SQLite +# --------------------------------------------------------------------------- + + +def test_seed_is_idempotent(): + """Calling seed() twice must not create duplicate rows (get_or_create). + + First call: all three tracks created (created=True for each). + Second call: all three already exist (created=False for each). + Both calls succeed without error. + """ + results_first = seed(model_class=EvolutionTrackStub) + assert len(results_first) == 3 + assert all(created for _, created in results_first) + + results_second = seed(model_class=EvolutionTrackStub) + assert len(results_second) == 3 + assert not any(created for _, created in results_second) + + assert EvolutionTrackStub.select().count() == 3