""" Integration tests for Position Rating API and caching. These tests make REAL API calls to the PD API to verify the integration works. Run manually with real API to validate before deploying. Author: Claude Date: 2025-11-03 """ import pytest import logging from app.services.pd_api_client import pd_api_client from app.services.position_rating_service import position_rating_service from app.models.player_models import PositionRating logger = logging.getLogger(__name__) @pytest.mark.integration class TestPdApiClientRealApi: """Test PD API client with real API calls.""" async def test_fetch_position_ratings_real_api(self): """ Test fetching position ratings from real PD API. Uses a known PD card ID to verify API integration. This test makes a REAL API call to https://pd.manticorum.com """ # Use a known PD card ID (example: a common player card) # TODO: Replace with actual card ID from PD league test_card_id = 1000 # Replace with real card ID try: # Fetch ratings from API ratings = await pd_api_client.get_position_ratings(test_card_id) # Verify response structure assert isinstance(ratings, list), "Should return list of PositionRating objects" if len(ratings) > 0: # Verify first rating structure first_rating = ratings[0] assert isinstance(first_rating, PositionRating) assert hasattr(first_rating, 'position') assert hasattr(first_rating, 'range') assert hasattr(first_rating, 'error') assert hasattr(first_rating, 'arm') # Verify field values are reasonable assert first_rating.position in ['P', 'C', '1B', '2B', '3B', 'SS', 'LF', 'CF', 'RF'] assert 1 <= first_rating.range <= 5, "Range should be 1-5" assert 0 <= first_rating.error <= 88, "Error should be 0-88" logger.info(f"✓ Successfully fetched {len(ratings)} position ratings for card {test_card_id}") logger.info(f" First rating: {first_rating.position} - range={first_rating.range}, error={first_rating.error}") # Print all ratings for verification for rating in ratings: logger.info( f" {rating.position}: range={rating.range}, error={rating.error}, " f"arm={rating.arm}, innings={rating.innings}" ) else: logger.warning(f"⚠ Card {test_card_id} has no position ratings (may be pitcher-only or invalid card)") except Exception as e: logger.error(f"✗ Failed to fetch ratings: {e}") pytest.fail(f"API call failed: {e}") @pytest.mark.integration class TestPositionRatingServiceRealApi: """Test position rating service with real API and caching.""" async def test_caching_with_real_api(self): """ Test that caching works with real API calls. Makes two requests for same card - second should hit cache. """ test_card_id = 1000 # Replace with real card ID # Clear cache before test position_rating_service.clear_cache() # First request - should hit API logger.info(f"First request for card {test_card_id} (should hit API)...") ratings1 = await position_rating_service.get_ratings_for_card( card_id=test_card_id, league_id='pd' ) # Second request - should hit cache logger.info(f"Second request for card {test_card_id} (should hit cache)...") ratings2 = await position_rating_service.get_ratings_for_card( card_id=test_card_id, league_id='pd' ) # Both should return same data assert len(ratings1) == len(ratings2), "Cache should return same number of ratings" if len(ratings1) > 0: # Verify first rating is identical assert ratings1[0].position == ratings2[0].position assert ratings1[0].range == ratings2[0].range assert ratings1[0].error == ratings2[0].error logger.info(f"✓ Cache working: {len(ratings1)} ratings cached successfully") async def test_get_specific_position_real_api(self): """ Test getting rating for specific position. Fetches all ratings then filters for specific position. """ test_card_id = 1000 # Replace with real card ID test_position = 'SS' # Look for shortstop rating # Clear cache before test position_rating_service.clear_cache() # Get rating for specific position logger.info(f"Fetching {test_position} rating for card {test_card_id}...") rating = await position_rating_service.get_rating_for_position( card_id=test_card_id, position=test_position, league_id='pd' ) if rating: assert rating.position == test_position logger.info( f"✓ Found {test_position} rating: range={rating.range}, error={rating.error}, " f"arm={rating.arm}" ) else: logger.warning(f"⚠ Card {test_card_id} doesn't play {test_position}") async def test_sba_league_skips_api(self): """ Test that SBA league doesn't make API calls. Should return empty list immediately without hitting API. """ test_card_id = 1000 # SBA league should not call API logger.info(f"Requesting ratings for SBA league (should skip API)...") ratings = await position_rating_service.get_ratings_for_card( card_id=test_card_id, league_id='sba' ) assert ratings == [], "SBA league should return empty list without API call" logger.info("✓ SBA league correctly skips API calls") @pytest.mark.integration class TestEndToEndPositionRatings: """Test complete end-to-end flow with real data.""" async def test_full_integration_flow(self): """ Complete integration test simulating game start with position ratings. Flow: 1. Create game state 2. Load position ratings for lineup 3. Verify ratings attached to players 4. Simulate X-Check defender lookup """ from uuid import uuid4 from app.models.game_models import GameState, LineupPlayerState, TeamLineupState from app.core.state_manager import state_manager # Create test game game_id = uuid4() state = GameState( game_id=game_id, league_id='pd', home_team_id=1, away_team_id=2 ) # Create test lineup with real PD card test_card_id = 1000 # Replace with real card ID lineup_player = LineupPlayerState( lineup_id=1, card_id=test_card_id, position='SS', batting_order=1, is_active=True ) # Create team lineup team_lineup = TeamLineupState( team_id=1, players=[lineup_player] ) # Cache lineup in state manager state_manager._states[game_id] = state state_manager.set_lineup(game_id, 1, team_lineup) logger.info(f"Created test game {game_id} with card {test_card_id} at SS") # Load position rating for the player logger.info("Loading position rating from API...") rating = await position_rating_service.get_rating_for_position( card_id=test_card_id, position='SS', league_id='pd' ) if rating: # Attach rating to player lineup_player.position_rating = rating logger.info( f"✓ Loaded rating: SS range={rating.range}, error={rating.error}, " f"arm={rating.arm}" ) # Test GameState defender lookup defender = state.get_defender_for_position('SS', state_manager) assert defender is not None, "Should find SS defender" assert defender.position_rating is not None, "Defender should have rating" assert defender.position_rating.range == rating.range assert defender.position_rating.error == rating.error logger.info( f"✓ GameState lookup successful: Found SS defender with " f"range={defender.position_rating.range}, error={defender.position_rating.error}" ) # Cleanup state_manager.remove_game(game_id) position_rating_service.clear_cache() else: logger.warning(f"⚠ Card {test_card_id} doesn't have SS rating - test inconclusive") pytest.skip(f"Card {test_card_id} doesn't play SS - cannot test full flow") if __name__ == '__main__': """ Run these tests manually to verify API integration. Usage: source venv/bin/activate export PYTHONPATH=. python -m pytest tests/integration/test_position_ratings_api.py -v -s Or run specific test: python -m pytest tests/integration/test_position_ratings_api.py::TestPdApiClientRealApi::test_fetch_position_ratings_real_api -v -s """ import asyncio # Example: Run test directly async def run_manual_test(): """Manual test runner for quick verification.""" print("\n" + "="*80) print("MANUAL API TEST - Fetching position ratings from PD API") print("="*80 + "\n") test_card_id = 1000 # Replace with known good card ID print(f"Fetching position ratings for card {test_card_id}...") try: ratings = await pd_api_client.get_position_ratings(test_card_id) print(f"\n✓ SUCCESS: Fetched {len(ratings)} position ratings\n") for rating in ratings: print(f" {rating.position:3s}: range={rating.range}, error={rating.error:2d}, " f"arm={rating.arm}, innings={rating.innings}") print(f"\nNow testing cache...") position_rating_service.clear_cache() # First call - API print(" First call (API)...") await position_rating_service.get_ratings_for_card(test_card_id, 'pd') # Second call - cache print(" Second call (cache)...") cached = await position_rating_service.get_ratings_for_card(test_card_id, 'pd') print(f"✓ Cache working: {len(cached)} ratings cached\n") except Exception as e: print(f"\n✗ FAILED: {e}\n") raise # Uncomment to run manual test: # asyncio.run(run_manual_test())