major-domo-v2/tests/test_services_custom_commands.py
Cal Corum 7b41520054 CLAUDE: Major bot enhancements - Admin commands, player stats, standings, schedules
Major Features Added:
• Admin Management System: Complete admin command suite with user moderation, system control, and bot maintenance tools
• Enhanced Player Commands: Added batting/pitching statistics with concurrent API calls and improved embed design
• League Standings: Full standings system with division grouping, playoff picture, and wild card visualization
• Game Schedules: Comprehensive schedule system with team filtering, series organization, and proper home/away indicators

New Admin Commands (12 total):
• /admin-status, /admin-help, /admin-reload, /admin-sync, /admin-clear
• /admin-announce, /admin-maintenance
• /admin-timeout, /admin-untimeout, /admin-kick, /admin-ban, /admin-unban, /admin-userinfo

Enhanced Player Display:
• Team logo positioned beside player name using embed author
• Smart thumbnail priority: fancycard → headshot → team logo fallback
• Concurrent batting/pitching stats fetching for performance
• Rich statistics display with team colors and comprehensive metrics

New Models & Services:
• BattingStats, PitchingStats, TeamStandings, Division, Game models
• StatsService, StandingsService, ScheduleService for data management
• CustomCommand system with CRUD operations and cleanup tasks

Bot Architecture Improvements:
• Admin commands integrated into bot.py with proper loading
• Permission checks and safety guards for moderation commands
• Enhanced error handling and comprehensive audit logging
• All 227 tests passing with new functionality

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-28 15:32:38 -05:00

245 lines
9.4 KiB
Python

"""
Tests for Custom Commands Service in Discord Bot v2.0
Fixed version with proper mocking following established patterns.
"""
import pytest
import asyncio
from datetime import datetime, timedelta, timezone
from unittest.mock import AsyncMock, MagicMock, patch
from typing import List
from services.custom_commands_service import (
CustomCommandsService,
CustomCommandNotFoundError,
CustomCommandExistsError,
CustomCommandPermissionError
)
from models.custom_command import (
CustomCommand,
CustomCommandCreator,
CustomCommandSearchFilters,
CustomCommandSearchResult,
CustomCommandStats
)
@pytest.fixture
def sample_creator() -> CustomCommandCreator:
"""Fixture providing a sample creator."""
return CustomCommandCreator(
id=1,
discord_id=12345,
username="testuser",
display_name="Test User",
created_at=datetime.now(timezone.utc),
total_commands=5,
active_commands=5
)
@pytest.fixture
def sample_command(sample_creator: CustomCommandCreator) -> CustomCommand:
"""Fixture providing a sample command."""
now = datetime.now(timezone.utc)
return CustomCommand(
id=1,
name="testcmd",
content="This is a test command response",
creator_id=sample_creator.id,
creator=sample_creator,
created_at=now,
updated_at=None,
last_used=now - timedelta(days=2),
use_count=10,
warning_sent=False,
is_active=True,
tags=None
)
@pytest.fixture
def mock_client():
"""Mock API client."""
client = AsyncMock()
return client
@pytest.fixture
def custom_commands_service_instance(mock_client):
"""Create CustomCommandsService instance with mocked client."""
service = CustomCommandsService()
service._client = mock_client
return service
class TestCustomCommandsServiceInit:
"""Test service initialization and basic functionality."""
def test_service_singleton_pattern(self):
"""Test that the service follows singleton pattern."""
from services.custom_commands_service import custom_commands_service
# Multiple imports should return the same instance
from services.custom_commands_service import custom_commands_service as service2
assert custom_commands_service is service2
def test_service_has_required_methods(self):
"""Test that service has all required methods."""
from services.custom_commands_service import custom_commands_service
# Core CRUD operations
assert hasattr(custom_commands_service, 'create_command')
assert hasattr(custom_commands_service, 'get_command_by_name')
assert hasattr(custom_commands_service, 'update_command')
assert hasattr(custom_commands_service, 'delete_command')
# Search and listing
assert hasattr(custom_commands_service, 'search_commands')
assert hasattr(custom_commands_service, 'get_commands_by_creator')
assert hasattr(custom_commands_service, 'get_command_names_for_autocomplete')
# Execution
assert hasattr(custom_commands_service, 'execute_command')
# Management
assert hasattr(custom_commands_service, 'get_statistics')
assert hasattr(custom_commands_service, 'get_commands_needing_warning')
assert hasattr(custom_commands_service, 'get_commands_eligible_for_deletion')
class TestCustomCommandsServiceCRUD:
"""Test CRUD operations of the custom commands service."""
@pytest.mark.asyncio
async def test_create_command_success(self, custom_commands_service_instance, sample_creator):
"""Test successful command creation."""
# Mock the service methods directly
created_command = None
async def mock_get_command_by_name(name, *args, **kwargs):
if created_command and name == "hello":
return created_command
# Command doesn't exist initially - raise exception
raise CustomCommandNotFoundError(f"Custom command '{name}' not found")
async def mock_get_or_create_creator(*args, **kwargs):
return sample_creator
async def mock_create(data):
nonlocal created_command
# Create the command model directly from the data
created_command = CustomCommand(
id=1,
name=data["name"],
content=data["content"],
creator_id=sample_creator.id,
creator=sample_creator,
created_at=datetime.now(timezone.utc),
updated_at=None,
last_used=datetime.now(timezone.utc),
use_count=0,
warning_sent=False,
is_active=True,
tags=None
)
return created_command
async def mock_update_creator_stats(*args, **kwargs):
return None
# Patch the service methods
custom_commands_service_instance.get_command_by_name = mock_get_command_by_name
custom_commands_service_instance.get_or_create_creator = mock_get_or_create_creator
custom_commands_service_instance.create = mock_create
custom_commands_service_instance._update_creator_stats = mock_update_creator_stats
result = await custom_commands_service_instance.create_command(
name="hello",
content="Hello, world!",
creator_discord_id=12345,
creator_username="testuser",
creator_display_name="Test User"
)
assert isinstance(result, CustomCommand)
assert result.name == "hello"
assert result.content == "Hello, world!"
assert result.creator.discord_id == 12345
assert result.use_count == 0
@pytest.mark.asyncio
async def test_create_command_already_exists(self, custom_commands_service_instance, sample_command):
"""Test command creation when command already exists."""
# Mock command already exists
async def mock_get_command_by_name(*args, **kwargs):
return sample_command
custom_commands_service_instance.get_command_by_name = mock_get_command_by_name
with pytest.raises(CustomCommandExistsError, match="Command 'hello' already exists"):
await custom_commands_service_instance.create_command(
name="hello",
content="Hello, world!",
creator_discord_id=12345,
creator_username="testuser"
)
@pytest.mark.asyncio
async def test_get_command_by_name_success(self, custom_commands_service_instance, sample_command, sample_creator):
"""Test successful command retrieval."""
# Mock the API client to return proper data structure
command_data = {
'id': sample_command.id,
'name': sample_command.name,
'content': sample_command.content,
'creator_id': sample_command.creator_id,
'creator': {
'id': sample_creator.id,
'discord_id': sample_creator.discord_id,
'username': sample_creator.username,
'display_name': sample_creator.display_name,
'created_at': sample_creator.created_at.isoformat(),
'total_commands': sample_creator.total_commands,
'active_commands': sample_creator.active_commands
},
'created_at': sample_command.created_at.isoformat(),
'updated_at': sample_command.updated_at.isoformat() if sample_command.updated_at else None,
'last_used': sample_command.last_used.isoformat() if sample_command.last_used else None,
'use_count': sample_command.use_count,
'warning_sent': sample_command.warning_sent,
'is_active': sample_command.is_active,
'tags': sample_command.tags
}
custom_commands_service_instance._client.get.return_value = command_data
result = await custom_commands_service_instance.get_command_by_name("testcmd")
assert isinstance(result, CustomCommand)
assert result.name == "testcmd"
assert result.use_count == 10
@pytest.mark.asyncio
async def test_get_command_by_name_not_found(self, custom_commands_service_instance):
"""Test command retrieval when command doesn't exist."""
# Mock the API client to return None (not found)
custom_commands_service_instance._client.get.return_value = None
with pytest.raises(CustomCommandNotFoundError, match="Custom command 'nonexistent' not found"):
await custom_commands_service_instance.get_command_by_name("nonexistent")
class TestCustomCommandsServiceErrorHandling:
"""Test error handling scenarios."""
@pytest.mark.asyncio
async def test_api_connection_error(self, custom_commands_service_instance):
"""Test handling of API connection errors."""
from exceptions import APIException, BotException
# Mock the API client to raise an APIException
custom_commands_service_instance._client.get.side_effect = APIException("Connection error")
with pytest.raises(BotException, match="Failed to retrieve command 'test'"):
await custom_commands_service_instance.get_command_by_name("test")