Complete rename of the card progression system from "Evolution" to "Refractor" across all code, routes, models, services, seeds, and tests. - Route prefix: /api/v2/evolution → /api/v2/refractor - Model classes: EvolutionTrack → RefractorTrack, etc. - 12 files renamed, 8 files content-edited - New migration to rename DB tables - 117 tests pass, no logic changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
133 lines
4.6 KiB
Python
133 lines
4.6 KiB
Python
"""Integration tests for the refractor track catalog API endpoints (WP-06).
|
|
|
|
Tests cover:
|
|
GET /api/v2/refractor/tracks
|
|
GET /api/v2/refractor/tracks/{track_id}
|
|
|
|
All tests require a live PostgreSQL connection (POSTGRES_HOST env var) and
|
|
assume the refractor schema migration (WP-04) has already been applied.
|
|
Tests auto-skip when POSTGRES_HOST is not set.
|
|
|
|
Test data is inserted via psycopg2 before the test module runs and deleted
|
|
afterwards so the tests are repeatable. ON CONFLICT keeps the table clean
|
|
even if a previous run did not complete teardown.
|
|
"""
|
|
|
|
import os
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
POSTGRES_HOST = os.environ.get("POSTGRES_HOST")
|
|
_skip_no_pg = pytest.mark.skipif(
|
|
not POSTGRES_HOST, reason="POSTGRES_HOST not set — integration tests skipped"
|
|
)
|
|
|
|
AUTH_HEADER = {"Authorization": f"Bearer {os.environ.get('API_TOKEN', 'test-token')}"}
|
|
|
|
_SEED_TRACKS = [
|
|
("Batter", "batter", "pa+tb*2", 37, 149, 448, 896),
|
|
("Starting Pitcher", "sp", "ip+k", 10, 40, 120, 240),
|
|
("Relief Pitcher", "rp", "ip+k", 3, 12, 35, 70),
|
|
]
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def seeded_tracks(pg_conn):
|
|
"""Insert three canonical evolution tracks; remove them after the module.
|
|
|
|
Uses ON CONFLICT DO UPDATE so the fixture is safe to run even if rows
|
|
already exist from a prior test run that did not clean up. Returns the
|
|
list of row IDs that were upserted.
|
|
"""
|
|
cur = pg_conn.cursor()
|
|
ids = []
|
|
for name, card_type, formula, t1, t2, t3, t4 in _SEED_TRACKS:
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO refractor_track
|
|
(name, card_type, formula, t1_threshold, t2_threshold, t3_threshold, t4_threshold)
|
|
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
|
ON CONFLICT (card_type) DO UPDATE SET
|
|
name = EXCLUDED.name,
|
|
formula = EXCLUDED.formula,
|
|
t1_threshold = EXCLUDED.t1_threshold,
|
|
t2_threshold = EXCLUDED.t2_threshold,
|
|
t3_threshold = EXCLUDED.t3_threshold,
|
|
t4_threshold = EXCLUDED.t4_threshold
|
|
RETURNING id
|
|
""",
|
|
(name, card_type, formula, t1, t2, t3, t4),
|
|
)
|
|
ids.append(cur.fetchone()[0])
|
|
pg_conn.commit()
|
|
yield ids
|
|
cur.execute("DELETE FROM refractor_track WHERE id = ANY(%s)", (ids,))
|
|
pg_conn.commit()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def client():
|
|
"""FastAPI TestClient backed by the real PostgreSQL database."""
|
|
from app.main import app
|
|
|
|
with TestClient(app) as c:
|
|
yield c
|
|
|
|
|
|
@_skip_no_pg
|
|
def test_list_tracks_returns_count_3(client, seeded_tracks):
|
|
"""GET /tracks returns all three tracks with count=3.
|
|
|
|
After seeding batter/sp/rp, the table should have exactly those three
|
|
rows (no other tracks are inserted by other test modules).
|
|
"""
|
|
resp = client.get("/api/v2/refractor/tracks", headers=AUTH_HEADER)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["count"] == 3
|
|
assert len(data["items"]) == 3
|
|
|
|
|
|
@_skip_no_pg
|
|
def test_filter_by_card_type(client, seeded_tracks):
|
|
"""card_type=sp filter returns exactly 1 track with card_type 'sp'."""
|
|
resp = client.get("/api/v2/refractor/tracks?card_type=sp", headers=AUTH_HEADER)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["count"] == 1
|
|
assert data["items"][0]["card_type"] == "sp"
|
|
|
|
|
|
@_skip_no_pg
|
|
def test_get_single_track_with_thresholds(client, seeded_tracks):
|
|
"""GET /tracks/{id} returns a track dict with formula and t1-t4 thresholds."""
|
|
track_id = seeded_tracks[0] # batter
|
|
resp = client.get(f"/api/v2/refractor/tracks/{track_id}", headers=AUTH_HEADER)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["card_type"] == "batter"
|
|
assert data["formula"] == "pa+tb*2"
|
|
for key in ("t1_threshold", "t2_threshold", "t3_threshold", "t4_threshold"):
|
|
assert key in data, f"Missing field: {key}"
|
|
assert data["t1_threshold"] == 37
|
|
assert data["t4_threshold"] == 896
|
|
|
|
|
|
@_skip_no_pg
|
|
def test_404_for_nonexistent_track(client, seeded_tracks):
|
|
"""GET /tracks/999999 returns 404 when the track does not exist."""
|
|
resp = client.get("/api/v2/refractor/tracks/999999", headers=AUTH_HEADER)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
@_skip_no_pg
|
|
def test_auth_required(client, seeded_tracks):
|
|
"""Requests without a Bearer token return 401 for both endpoints."""
|
|
resp_list = client.get("/api/v2/refractor/tracks")
|
|
assert resp_list.status_code == 401
|
|
|
|
track_id = seeded_tracks[0]
|
|
resp_single = client.get(f"/api/v2/refractor/tracks/{track_id}")
|
|
assert resp_single.status_code == 401
|