paper-dynasty-gameplay-webapp/tests/unit/models/test_player.py
Cal Corum 559fe73f07 CLAUDE: Complete Player model migration with service layer and integration test infrastructure
## Player Model Migration
- Migrate Player model from Discord app following Model/Service Architecture pattern
- Extract all business logic from Player model to PlayerService
- Create pure data model with PostgreSQL relationships (Cardset, PositionRating)
- Implement comprehensive PlayerFactory with specialized methods for test data

## PlayerService Implementation
- Extract 5 business logic methods from original Player model:
  - get_batter_card_url() - batting card URL retrieval
  - get_pitcher_card_url() - pitching card URL retrieval
  - generate_name_card_link() - markdown link generation
  - get_formatted_name_with_description() - name formatting
  - get_player_description() - description from object or dict
- Follow BaseService pattern with dependency injection and logging

## Comprehensive Testing
- 35 passing Player tests (14 model + 21 service tests)
- PlayerFactory with specialized methods (batting/pitching cards, positions)
- Test isolation following factory pattern and db_session guidelines
- Fix PostgreSQL integer overflow in test ID generation

## Integration Test Infrastructure
- Create integration test framework for improving service coverage
- Design AIService integration tests targeting uncovered branches
- Demonstrate real database query testing with proper isolation
- Establish patterns for testing complex game scenarios

## Service Coverage Analysis
- Current service coverage: 61% overall
- PlayerService: 100% coverage (excellent migration example)
- AIService: 60% coverage (improvement opportunities identified)
- Integration test strategy designed to achieve 90%+ coverage

## Database Integration
- Update Cardset model to include players relationship
- Update PositionRating model with proper Player foreign key
- Maintain all existing relationships and constraints
- Demonstrate data isolation and automatic cleanup in tests

## Test Suite Status
- 137 tests passing, 0 failures (maintained 100% pass rate)
- Added 35 new tests while preserving all existing functionality
- Integration test infrastructure ready for coverage improvements

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 16:20:29 -05:00

313 lines
11 KiB
Python

"""
Unit tests for Player model.
Tests the pure data model functionality, field validation,
and database relationships following test isolation guidelines.
"""
import datetime
import pytest
from sqlalchemy.exc import IntegrityError
from app.models.player import Player, PlayerBase
from tests.factories.player_factory import PlayerFactory
class TestPlayerBase:
"""Test PlayerBase model functionality."""
def test_create_player_base_with_required_fields(self):
"""Test creating PlayerBase with minimum required fields."""
player_data = {
"id": None, # Explicitly set to None since it's Optional but Pydantic requires explicit None
"name": "Test Player",
"cost": 25,
"image": "https://example.com/test.jpg",
"mlbclub": "LAD",
"franchise": "LAD",
"set_num": 1,
"pos_1": "C",
"description": "2023"
}
player = PlayerBase(**player_data)
assert player.name == "Test Player"
assert player.cost == 25
assert player.image == "https://example.com/test.jpg"
assert player.mlbclub == "LAD"
assert player.franchise == "LAD"
assert player.set_num == 1
assert player.pos_1 == "C"
assert player.description == "2023"
def test_player_base_defaults(self):
"""Test PlayerBase default values."""
player_data = {
"id": None,
"name": "Test Player",
"cost": 25,
"image": "https://example.com/test.jpg",
"mlbclub": "LAD",
"franchise": "LAD",
"set_num": 1,
"pos_1": "C",
"description": "2023"
}
player = PlayerBase(**player_data)
assert player.quantity == 999
assert player.image2 is None
assert player.pos_2 is None
assert player.pos_3 is None
assert player.pos_4 is None
assert player.pos_5 is None
assert player.pos_6 is None
assert player.pos_7 is None
assert player.pos_8 is None
assert player.headshot is None
assert player.vanity_card is None
assert player.strat_code is None
assert player.bbref_id is None
assert player.fangr_id is None
assert player.mlbplayer_id is None
assert isinstance(player.created, datetime.datetime)
def test_position_field_validation_uppercase(self):
"""Test that position fields are converted to uppercase."""
player_data = {
"id": None,
"name": "Test Player",
"cost": 25,
"image": "https://example.com/test.jpg",
"mlbclub": "LAD",
"franchise": "LAD",
"set_num": 1,
"pos_1": "c", # lowercase
"pos_2": "1b", # lowercase
"description": "2023"
}
player = PlayerBase(**player_data)
assert player.pos_1 == "C"
assert player.pos_2 == "1B"
def test_position_field_validation_none_values(self):
"""Test that None position values are preserved."""
player_data = {
"id": None,
"name": "Test Player",
"cost": 25,
"image": "https://example.com/test.jpg",
"mlbclub": "LAD",
"franchise": "LAD",
"set_num": 1,
"pos_1": "C",
"description": "2023"
}
player = PlayerBase(**player_data)
# None values should remain None
assert player.pos_2 is None
assert player.pos_3 is None
def test_all_position_fields_uppercase(self):
"""Test all position fields are converted to uppercase."""
player_data = {
"id": None,
"name": "Test Player",
"cost": 25,
"image": "https://example.com/test.jpg",
"mlbclub": "LAD",
"franchise": "LAD",
"set_num": 1,
"pos_1": "c",
"pos_2": "1b",
"pos_3": "2b",
"pos_4": "3b",
"pos_5": "ss",
"pos_6": "lf",
"pos_7": "cf",
"pos_8": "rf",
"description": "2023"
}
player = PlayerBase(**player_data)
assert player.pos_1 == "C"
assert player.pos_2 == "1B"
assert player.pos_3 == "2B"
assert player.pos_4 == "3B"
assert player.pos_5 == "SS"
assert player.pos_6 == "LF"
assert player.pos_7 == "CF"
assert player.pos_8 == "RF"
class TestPlayerModel:
"""Test Player model database functionality."""
def test_create_player_in_database(self, db_session):
"""Test creating and saving a Player to database."""
player = PlayerFactory.create(db_session, name="Database Player")
assert player.id is not None
assert player.name == "Database Player"
# Verify it exists in database
retrieved = db_session.get(Player, player.id)
assert retrieved is not None
assert retrieved.name == "Database Player"
def test_player_unique_id_constraint(self, db_session):
"""Test that player IDs must be unique."""
player1 = PlayerFactory.create(db_session, name="Player One")
player1_id = player1.id
# Attempt to create player with same ID should fail
with pytest.raises(IntegrityError):
duplicate_player = Player(
id=player1_id,
name="Player Two",
cost=20,
image="https://example.com/test2.jpg",
mlbclub="NYY",
franchise="NYY",
set_num=2,
pos_1="1B",
description="2022"
)
db_session.add(duplicate_player)
db_session.commit()
def test_player_with_cardset_relationship(self, db_session):
"""Test Player relationship with Cardset."""
from tests.factories.cardset_factory import CardsetFactory
cardset = CardsetFactory.create(db_session, name="Test Set")
player = PlayerFactory.create(
db_session,
name="Related Player",
cardset_id=cardset.id
)
assert player.cardset_id == cardset.id
# Relationship should be accessible (when cardset is properly loaded)
db_session.refresh(player)
assert player.cardset is not None
assert player.cardset.name == "Test Set"
def test_player_factory_methods(self, db_session):
"""Test PlayerFactory convenience methods."""
# Test batting card factory
batter = PlayerFactory.create_with_batting_card(db_session)
assert "batting" in batter.image
assert batter.image2 is None
# Test pitching card factory
pitcher = PlayerFactory.create_with_pitching_card(db_session)
assert "pitching" in pitcher.image
assert pitcher.image2 is None
# Test both cards factory
both = PlayerFactory.create_with_both_cards(db_session)
assert "batting" in both.image
assert "pitching" in both.image2
# Test position-specific factories
catcher = PlayerFactory.create_catcher(db_session)
assert catcher.pos_1 == "C"
pitcher = PlayerFactory.create_pitcher(db_session)
assert pitcher.pos_1 == "P"
assert "pitching" in pitcher.image
class TestPlayerBusinessLogicRemoval:
"""Test that business logic has been properly removed from model."""
def test_no_business_logic_methods(self, db_session):
"""Test that business logic methods are not present in Player model."""
player = PlayerFactory.create(db_session)
# These methods should NOT exist (moved to PlayerService)
assert not hasattr(player, 'batter_card_url')
assert not hasattr(player, 'pitcher_card_url')
assert not hasattr(player, 'name_card_link')
assert not hasattr(player, 'name_with_desc')
def test_player_is_pure_data_model(self, db_session):
"""Test that Player model only contains data fields and relationships."""
player = PlayerFactory.create(db_session)
# Should have data fields
assert hasattr(player, 'name')
assert hasattr(player, 'cost')
assert hasattr(player, 'image')
assert hasattr(player, 'mlbclub')
# Should have relationships (defined in class)
assert hasattr(Player, 'cardset')
# assert hasattr(Player, 'cards') # Commented out until Card model is created
# assert hasattr(Player, 'lineups') # Commented out until Lineup model is created
assert hasattr(Player, 'positions')
# Should NOT have business logic methods
methods = [method for method in dir(player) if not method.startswith('_')]
business_methods = ['batter_card_url', 'pitcher_card_url', 'name_card_link', 'name_with_desc']
for method in business_methods:
assert method not in methods, f"Business logic method {method} should be moved to service"
class TestPlayerDataIntegrity:
"""Test Player model data integrity and constraints."""
def test_player_required_fields(self, db_session):
"""Test that required fields exist in the model."""
# Since SQLModel table models don't raise validation errors for missing fields
# at instantiation time, we test by checking the required field exists
player = PlayerFactory.create(db_session)
# These fields should always be present
assert hasattr(player, 'name')
assert hasattr(player, 'cost')
assert hasattr(player, 'image')
assert hasattr(player, 'mlbclub')
assert hasattr(player, 'franchise')
assert hasattr(player, 'set_num')
assert hasattr(player, 'pos_1')
assert hasattr(player, 'description')
def test_player_field_types(self, db_session):
"""Test that field types are enforced."""
player = PlayerFactory.create(db_session)
assert isinstance(player.name, str)
assert isinstance(player.cost, int)
assert isinstance(player.image, str)
assert isinstance(player.created, datetime.datetime)
assert isinstance(player.quantity, int)
def test_player_optional_fields(self, db_session):
"""Test that optional fields can be None."""
player = PlayerFactory.create(
db_session,
image2=None,
headshot=None,
vanity_card=None,
strat_code=None,
bbref_id=None,
fangr_id=None,
mlbplayer_id=None
)
assert player.image2 is None
assert player.headshot is None
assert player.vanity_card is None
assert player.strat_code is None
assert player.bbref_id is None
assert player.fangr_id is None
assert player.mlbplayer_id is None