- Add DraftSheetService with write_pick(), write_picks_batch(), clear_picks_range(), and get_sheet_url() methods - Integrate sheet writes in /draft command (fire-and-forget pattern) - Integrate sheet writes in draft_monitor.py for auto-draft picks - Add /draft-admin resync-sheet command for bulk recovery - Add sheet link to /draft-status embed - Add draft_sheet_keys config with env var overrides per season - Add get_picks_with_players() to draft_pick_service for resync - Add 13 unit tests for DraftSheetService (all passing) - Update CLAUDE.md documentation files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
350 lines
10 KiB
Python
350 lines
10 KiB
Python
"""
|
|
Tests for DraftSheetService
|
|
|
|
Tests the Google Sheets integration for draft pick tracking.
|
|
Uses mocked pygsheets to avoid actual API calls.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
from typing import Tuple, List
|
|
|
|
from services.draft_sheet_service import DraftSheetService, get_draft_sheet_service
|
|
|
|
|
|
class TestDraftSheetService:
|
|
"""
|
|
Test suite for DraftSheetService.
|
|
|
|
Tests write_pick(), write_picks_batch(), clear_picks_range(), and get_sheet_url().
|
|
All tests mock pygsheets to avoid actual Google Sheets API calls.
|
|
"""
|
|
|
|
@pytest.fixture
|
|
def mock_config(self):
|
|
"""
|
|
Create a mock config with draft sheet settings.
|
|
|
|
Provides:
|
|
- draft_sheet_enabled: True
|
|
- sba_season: 12
|
|
- draft_sheet_worksheet: "Ordered List"
|
|
- draft_sheet_start_column: "D"
|
|
- draft_total_picks: 512
|
|
"""
|
|
config = MagicMock()
|
|
config.draft_sheet_enabled = True
|
|
config.sba_season = 12
|
|
config.draft_sheet_worksheet = "Ordered List"
|
|
config.draft_sheet_start_column = "D"
|
|
config.draft_total_picks = 512
|
|
config.sheets_credentials_path = "/app/data/test-creds.json"
|
|
config.get_draft_sheet_key = MagicMock(return_value="test-sheet-key-123")
|
|
config.get_draft_sheet_url = MagicMock(
|
|
return_value="https://docs.google.com/spreadsheets/d/test-sheet-key-123"
|
|
)
|
|
return config
|
|
|
|
@pytest.fixture
|
|
def mock_pygsheets(self):
|
|
"""
|
|
Create mock pygsheets client, spreadsheet, and worksheet.
|
|
|
|
Provides:
|
|
- sheets_client: Mock pygsheets client
|
|
- spreadsheet: Mock spreadsheet
|
|
- worksheet: Mock worksheet with update_values method
|
|
"""
|
|
worksheet = MagicMock()
|
|
worksheet.update_values = MagicMock()
|
|
|
|
spreadsheet = MagicMock()
|
|
spreadsheet.worksheet_by_title = MagicMock(return_value=worksheet)
|
|
|
|
sheets_client = MagicMock()
|
|
sheets_client.open_by_key = MagicMock(return_value=spreadsheet)
|
|
|
|
return {
|
|
'client': sheets_client,
|
|
'spreadsheet': spreadsheet,
|
|
'worksheet': worksheet
|
|
}
|
|
|
|
@pytest.fixture
|
|
def service(self, mock_config, mock_pygsheets):
|
|
"""
|
|
Create DraftSheetService instance with mocked dependencies.
|
|
|
|
The service is set up with:
|
|
- Mocked config
|
|
- Mocked pygsheets client (via _get_client override)
|
|
"""
|
|
with patch('services.draft_sheet_service.get_config', return_value=mock_config):
|
|
service = DraftSheetService()
|
|
service._config = mock_config
|
|
service._sheets_client = mock_pygsheets['client']
|
|
return service
|
|
|
|
# ==================== write_pick() Tests ====================
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_pick_success(self, service, mock_pygsheets):
|
|
"""
|
|
Test successful write of a single draft pick to the sheet.
|
|
|
|
Verifies:
|
|
- Correct cell range is calculated (D + overall + 1)
|
|
- Correct data is written (4 columns)
|
|
- Returns True on success
|
|
"""
|
|
result = await service.write_pick(
|
|
season=12,
|
|
overall=1,
|
|
orig_owner_abbrev="HAM",
|
|
owner_abbrev="HAM",
|
|
player_name="Mike Trout",
|
|
swar=8.5
|
|
)
|
|
|
|
assert result is True
|
|
# Verify worksheet was accessed
|
|
mock_pygsheets['spreadsheet'].worksheet_by_title.assert_called_with("Ordered List")
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_pick_disabled(self, service, mock_config):
|
|
"""
|
|
Test that write_pick returns False when feature is disabled.
|
|
|
|
Verifies:
|
|
- Returns False when draft_sheet_enabled is False
|
|
- No API calls are made
|
|
"""
|
|
mock_config.draft_sheet_enabled = False
|
|
|
|
result = await service.write_pick(
|
|
season=12,
|
|
overall=1,
|
|
orig_owner_abbrev="HAM",
|
|
owner_abbrev="HAM",
|
|
player_name="Mike Trout",
|
|
swar=8.5
|
|
)
|
|
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_pick_no_sheet_configured(self, service, mock_config):
|
|
"""
|
|
Test that write_pick returns False when no sheet is configured for season.
|
|
|
|
Verifies:
|
|
- Returns False when get_draft_sheet_key returns None
|
|
- No API calls are made
|
|
"""
|
|
mock_config.get_draft_sheet_key = MagicMock(return_value=None)
|
|
|
|
result = await service.write_pick(
|
|
season=13, # Season 13 has no configured sheet
|
|
overall=1,
|
|
orig_owner_abbrev="HAM",
|
|
owner_abbrev="HAM",
|
|
player_name="Mike Trout",
|
|
swar=8.5
|
|
)
|
|
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_pick_api_error(self, service, mock_pygsheets):
|
|
"""
|
|
Test that write_pick returns False and logs error on API failure.
|
|
|
|
Verifies:
|
|
- Returns False on exception
|
|
- Exception is caught and logged (not raised)
|
|
"""
|
|
mock_pygsheets['spreadsheet'].worksheet_by_title.side_effect = Exception("API Error")
|
|
|
|
result = await service.write_pick(
|
|
season=12,
|
|
overall=1,
|
|
orig_owner_abbrev="HAM",
|
|
owner_abbrev="HAM",
|
|
player_name="Mike Trout",
|
|
swar=8.5
|
|
)
|
|
|
|
assert result is False
|
|
|
|
# ==================== write_picks_batch() Tests ====================
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_picks_batch_success(self, service, mock_pygsheets):
|
|
"""
|
|
Test successful batch write of multiple picks.
|
|
|
|
Verifies:
|
|
- All picks are written
|
|
- Returns correct success/failure counts
|
|
"""
|
|
picks = [
|
|
(1, "HAM", "HAM", "Player 1", 2.5),
|
|
(2, "NYY", "NYY", "Player 2", 3.0),
|
|
(3, "BOS", "BOS", "Player 3", 1.5),
|
|
]
|
|
|
|
success_count, failure_count = await service.write_picks_batch(
|
|
season=12,
|
|
picks=picks
|
|
)
|
|
|
|
assert success_count == 3
|
|
assert failure_count == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_picks_batch_empty_list(self, service):
|
|
"""
|
|
Test batch write with empty picks list.
|
|
|
|
Verifies:
|
|
- Returns (0, 0) for empty list
|
|
- No API calls are made
|
|
"""
|
|
success_count, failure_count = await service.write_picks_batch(
|
|
season=12,
|
|
picks=[]
|
|
)
|
|
|
|
assert success_count == 0
|
|
assert failure_count == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_write_picks_batch_disabled(self, service, mock_config):
|
|
"""
|
|
Test batch write when feature is disabled.
|
|
|
|
Verifies:
|
|
- Returns (0, total_picks) when disabled
|
|
"""
|
|
mock_config.draft_sheet_enabled = False
|
|
picks = [
|
|
(1, "HAM", "HAM", "Player 1", 2.5),
|
|
(2, "NYY", "NYY", "Player 2", 3.0),
|
|
]
|
|
|
|
success_count, failure_count = await service.write_picks_batch(
|
|
season=12,
|
|
picks=picks
|
|
)
|
|
|
|
assert success_count == 0
|
|
assert failure_count == 2
|
|
|
|
# ==================== clear_picks_range() Tests ====================
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clear_picks_range_success(self, service, mock_pygsheets):
|
|
"""
|
|
Test successful clearing of picks range.
|
|
|
|
Verifies:
|
|
- Returns True on success
|
|
- Correct range is cleared
|
|
"""
|
|
result = await service.clear_picks_range(
|
|
season=12,
|
|
start_overall=1,
|
|
end_overall=512
|
|
)
|
|
|
|
assert result is True
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_clear_picks_range_disabled(self, service, mock_config):
|
|
"""
|
|
Test clearing when feature is disabled.
|
|
|
|
Verifies:
|
|
- Returns False when disabled
|
|
"""
|
|
mock_config.draft_sheet_enabled = False
|
|
|
|
result = await service.clear_picks_range(
|
|
season=12,
|
|
start_overall=1,
|
|
end_overall=512
|
|
)
|
|
|
|
assert result is False
|
|
|
|
# ==================== get_sheet_url() Tests ====================
|
|
|
|
def test_get_sheet_url_configured(self, service, mock_config):
|
|
"""
|
|
Test get_sheet_url returns URL when configured.
|
|
|
|
Verifies:
|
|
- Returns correct URL format
|
|
"""
|
|
url = service.get_sheet_url(season=12)
|
|
|
|
assert url == "https://docs.google.com/spreadsheets/d/test-sheet-key-123"
|
|
|
|
def test_get_sheet_url_not_configured(self, service, mock_config):
|
|
"""
|
|
Test get_sheet_url returns None when not configured.
|
|
|
|
Verifies:
|
|
- Returns None for unconfigured season
|
|
"""
|
|
mock_config.get_draft_sheet_url = MagicMock(return_value=None)
|
|
|
|
url = service.get_sheet_url(season=99)
|
|
|
|
assert url is None
|
|
|
|
|
|
class TestGlobalServiceInstance:
|
|
"""
|
|
Test suite for the global service instance pattern.
|
|
|
|
Tests get_draft_sheet_service() lazy initialization.
|
|
"""
|
|
|
|
def test_get_draft_sheet_service_returns_instance(self):
|
|
"""
|
|
Test that get_draft_sheet_service returns a DraftSheetService instance.
|
|
|
|
Note: This creates a real service instance but won't make API calls
|
|
without being used.
|
|
"""
|
|
with patch('services.draft_sheet_service.get_config') as mock_config:
|
|
mock_config.return_value.sheets_credentials_path = "/test/path.json"
|
|
mock_config.return_value.draft_sheet_enabled = True
|
|
|
|
# Reset global instance
|
|
import services.draft_sheet_service as service_module
|
|
service_module._draft_sheet_service = None
|
|
|
|
service = get_draft_sheet_service()
|
|
|
|
assert isinstance(service, DraftSheetService)
|
|
|
|
def test_get_draft_sheet_service_returns_same_instance(self):
|
|
"""
|
|
Test that get_draft_sheet_service returns the same instance on subsequent calls.
|
|
|
|
Verifies singleton pattern for global service.
|
|
"""
|
|
with patch('services.draft_sheet_service.get_config') as mock_config:
|
|
mock_config.return_value.sheets_credentials_path = "/test/path.json"
|
|
mock_config.return_value.draft_sheet_enabled = True
|
|
|
|
# Reset global instance
|
|
import services.draft_sheet_service as service_module
|
|
service_module._draft_sheet_service = None
|
|
|
|
service1 = get_draft_sheet_service()
|
|
service2 = get_draft_sheet_service()
|
|
|
|
assert service1 is service2
|