## 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>
313 lines
11 KiB
Python
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 |