- Add SQLITE_DB_PATH env var to db_engine.py for test isolation - Create tests/conftest.py with in-memory SQLite fixture and sample data helpers - Add tests/test_dependencies.py: unit tests for valid_token, mround, param_char, get_req_url - Add tests/test_card_pricing.py: tests for Player.change_on_sell/buy and get_all_pos - Add tests/test_api_packs.py: integration tests for GET/POST/DELETE /api/v2/packs - Add requirements-test.txt with pytest and httpx - Add test job to CI workflow (build now requires tests to pass first) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
117 lines
4.5 KiB
Python
117 lines
4.5 KiB
Python
"""
|
|
Unit tests for card pricing logic in Player model.
|
|
|
|
Player.change_on_sell() and Player.change_on_buy() are critical business logic
|
|
used whenever cards are traded. These tests verify the price update math and
|
|
the floor/ceiling behaviour.
|
|
"""
|
|
|
|
import math
|
|
|
|
import pytest
|
|
|
|
|
|
class TestPlayerChangeOnSell:
|
|
"""Tests for Player.change_on_sell() — price decreases 5% on each sale."""
|
|
|
|
def test_sell_reduces_cost_by_5_percent(self, sample_player):
|
|
"""Selling a card should reduce its cost to floor(cost * 0.95)."""
|
|
sample_player.cost = 100
|
|
sample_player.change_on_sell()
|
|
assert sample_player.cost == math.floor(100 * 0.95) # 95
|
|
|
|
def test_sell_saves_to_db(self, sample_player):
|
|
"""change_on_sell() should persist the new price to the database."""
|
|
from app.db_engine import Player
|
|
|
|
sample_player.cost = 200
|
|
sample_player.change_on_sell()
|
|
refreshed = Player.get_by_id(sample_player.pk)
|
|
assert refreshed.cost == math.floor(200 * 0.95) # 190
|
|
|
|
def test_sell_floors_at_1(self, sample_player):
|
|
"""Price should never drop below 1, even at very low starting values."""
|
|
sample_player.cost = 1
|
|
sample_player.change_on_sell()
|
|
assert sample_player.cost == 1
|
|
|
|
def test_sell_large_price(self, sample_player):
|
|
"""Large prices should still apply the 5% reduction correctly."""
|
|
sample_player.cost = 10000
|
|
sample_player.change_on_sell()
|
|
assert sample_player.cost == math.floor(10000 * 0.95) # 9500
|
|
|
|
def test_sell_rounds_down(self, sample_player):
|
|
"""floor() means fractional results are rounded down, not up."""
|
|
sample_player.cost = 21 # 21 * 0.95 = 19.95 → floor → 19
|
|
sample_player.change_on_sell()
|
|
assert sample_player.cost == 19
|
|
|
|
|
|
class TestPlayerChangeOnBuy:
|
|
"""Tests for Player.change_on_buy() — price increases 10% on each purchase."""
|
|
|
|
def test_buy_increases_cost_by_10_percent(self, sample_player):
|
|
"""Buying a card should increase its cost to ceil(cost * 1.1)."""
|
|
sample_player.cost = 100
|
|
sample_player.change_on_buy()
|
|
assert sample_player.cost == math.ceil(100 * 1.1) # 110
|
|
|
|
def test_buy_saves_to_db(self, sample_player):
|
|
"""change_on_buy() should persist the new price to the database."""
|
|
from app.db_engine import Player
|
|
|
|
sample_player.cost = 200
|
|
sample_player.change_on_buy()
|
|
refreshed = Player.get_by_id(sample_player.pk)
|
|
assert refreshed.cost == math.ceil(200 * 1.1) # 220
|
|
|
|
def test_buy_from_low_price(self, sample_player):
|
|
"""Low prices should still apply the 10% increase."""
|
|
sample_player.cost = 1
|
|
sample_player.change_on_buy()
|
|
assert sample_player.cost == math.ceil(1 * 1.1) # 2 (ceil of 1.1)
|
|
|
|
def test_buy_rounds_up(self, sample_player):
|
|
"""ceil() means fractional results are rounded up."""
|
|
sample_player.cost = 9 # 9 * 1.1 = 9.9 → ceil → 10
|
|
sample_player.change_on_buy()
|
|
assert sample_player.cost == 10
|
|
|
|
def test_buy_large_price(self, sample_player):
|
|
"""Large prices should still apply the 10% increase correctly."""
|
|
sample_player.cost = 5000
|
|
sample_player.change_on_buy()
|
|
assert sample_player.cost == math.ceil(5000 * 1.1) # 5500
|
|
|
|
|
|
class TestPlayerGetAllPos:
|
|
"""Tests for Player.get_all_pos() — returns non-null, non-CP position list."""
|
|
|
|
def test_returns_primary_position(self, sample_player):
|
|
"""A player with only pos_1 set should return a list with one position."""
|
|
sample_player.pos_1 = "1B"
|
|
assert sample_player.get_all_pos() == ["1B"]
|
|
|
|
def test_excludes_cp_position(self, sample_player):
|
|
"""CP (closing pitcher) is excluded from the position list."""
|
|
sample_player.pos_1 = "SP"
|
|
sample_player.pos_2 = "CP"
|
|
positions = sample_player.get_all_pos()
|
|
assert "CP" not in positions
|
|
assert "SP" in positions
|
|
|
|
def test_excludes_null_positions(self, sample_player):
|
|
"""None positions should not appear in the result."""
|
|
sample_player.pos_1 = "CF"
|
|
sample_player.pos_2 = None
|
|
assert sample_player.get_all_pos() == ["CF"]
|
|
|
|
def test_multiple_positions(self, sample_player):
|
|
"""Players with multiple eligible positions should return all of them."""
|
|
sample_player.pos_1 = "1B"
|
|
sample_player.pos_2 = "OF"
|
|
sample_player.pos_3 = "DH"
|
|
positions = sample_player.get_all_pos()
|
|
assert positions == ["1B", "OF", "DH"]
|