diff --git a/tests/test_admin_commands.py b/tests/test_admin_commands.py index 105a13a..886e91b 100644 --- a/tests/test_admin_commands.py +++ b/tests/test_admin_commands.py @@ -12,6 +12,7 @@ ) from typer_bot.commands.admin_panel import OpenFixtureWarningView from typer_bot.services.admin_service import FixtureScoreResult +from typer_bot.utils import now from typer_bot.utils.permissions import is_admin @@ -322,6 +323,29 @@ async def test_results_calculate_does_not_record_cooldown_when_scoring_fails( admin_cog._create_backup.assert_not_called() admin_cog._post_calculation_to_channel.assert_not_called() + @pytest.mark.asyncio + async def test_results_calculate_rejects_active_cooldown( + self, admin_cog, mock_interaction_admin + ): + """Cooldown should stop duplicate clicks before any scoring work starts.""" + user_id = str(mock_interaction_admin.user.id) + admin_cog.workflow_state.record_calculate_cooldown(user_id, current_time=now().timestamp()) + admin_cog.service.calculate_fixture_scores = AsyncMock() + + await admin_cog.results_calculate.callback(admin_cog, mock_interaction_admin, None) + + assert "Please wait" in mock_interaction_admin.response_sent[-1]["content"] + admin_cog.service.calculate_fixture_scores.assert_not_called() + + @pytest.mark.asyncio + async def test_results_calculate_reports_missing_open_fixture( + self, admin_cog, mock_interaction_admin + ): + """Missing fixture selection should fail through the slash-command response path.""" + await admin_cog.results_calculate.callback(admin_cog, mock_interaction_admin, None) + + assert mock_interaction_admin.response_sent[-1]["content"] == "No open fixtures found!" + class TestResultsPostFlow: @pytest.fixture diff --git a/tests/test_user_commands.py b/tests/test_user_commands.py index a04aaad..2742333 100644 --- a/tests/test_user_commands.py +++ b/tests/test_user_commands.py @@ -1,11 +1,13 @@ """Tests for user command wiring.""" +from datetime import UTC, datetime, timedelta from unittest.mock import AsyncMock, MagicMock import discord import pytest from typer_bot.commands.user_commands import UserCommands +from typer_bot.utils import format_standings @pytest.fixture @@ -48,3 +50,150 @@ async def test_handles_dm_permission_error(self, user_commands, mock_interaction assert len(mock_interaction.followup_sent) == 1 assert "can't send you DMs" in mock_interaction.followup_sent[0]["content"] + + +class TestFixturesCommand: + @pytest.mark.asyncio + async def test_no_open_fixture_shows_error(self, user_commands, mock_interaction): + await user_commands.fixtures.callback(user_commands, mock_interaction) + + assert mock_interaction.response_sent[0]["content"] == "❌ No active fixture found!" + + @pytest.mark.asyncio + async def test_single_open_fixture_lists_games_and_deadline( + self, user_commands, mock_interaction, database, sample_games + ): + await database.create_fixture(1, sample_games, datetime.now(UTC) + timedelta(days=1)) + + await user_commands.fixtures.callback(user_commands, mock_interaction) + + content = mock_interaction.response_sent[0]["content"] + assert "Week 1 Fixtures" in content + assert sample_games[0] in content + assert "Deadline:" in content + assert mock_interaction.response_sent[0]["ephemeral"] is True + + @pytest.mark.asyncio + async def test_multiple_open_fixtures_list_each_week( + self, user_commands, mock_interaction, database, sample_games + ): + deadline = datetime.now(UTC) + timedelta(days=1) + await database.create_fixture(1, sample_games, deadline) + await database.create_fixture(2, sample_games, deadline) + + await user_commands.fixtures.callback(user_commands, mock_interaction) + + content = mock_interaction.response_sent[0]["content"] + assert "Open Fixtures" in content + assert "Week 1" in content + assert "Week 2" in content + + +class TestStandingsCommand: + @pytest.mark.asyncio + async def test_standings_sends_empty_state(self, user_commands, mock_interaction): + await user_commands.standings.callback(user_commands, mock_interaction) + + assert mock_interaction.response_sent[0]["content"] == format_standings([], None) + assert mock_interaction.response_sent[0]["ephemeral"] is True + + @pytest.mark.asyncio + async def test_standings_sends_formatted_leaderboard(self, user_commands, mock_interaction): + standings = [ + { + "user_id": "123", + "user_name": "User1", + "total_points": 9, + "total_exact": 3, + "total_correct": 3, + } + ] + last_fixture = { + "week_number": 4, + "games": ["A - B"], + "results": ["2-1"], + "scores": [ + { + "user_id": "123", + "user_name": "User1", + "points": 3, + "exact_scores": 1, + "correct_results": 1, + } + ], + } + user_commands.db.get_standings = AsyncMock(return_value=standings) + user_commands.db.get_last_fixture_scores = AsyncMock(return_value=last_fixture) + + await user_commands.standings.callback(user_commands, mock_interaction) + + assert mock_interaction.response_sent[0]["content"] == format_standings( + standings, last_fixture + ) + + +class TestMyPredictionsCommand: + @pytest.mark.asyncio + async def test_no_open_fixture_shows_error(self, user_commands, mock_interaction): + await user_commands.my_predictions.callback(user_commands, mock_interaction) + + assert mock_interaction.response_sent[0]["content"] == "❌ No active fixture found!" + + @pytest.mark.asyncio + async def test_single_fixture_without_prediction_shows_prompt( + self, user_commands, mock_interaction, database, sample_games + ): + await database.create_fixture(1, sample_games, datetime.now(UTC) + timedelta(days=1)) + + await user_commands.my_predictions.callback(user_commands, mock_interaction) + + content = mock_interaction.response_sent[0]["content"] + assert "haven't submitted predictions" in content + assert "Use `/predict`" in content + + @pytest.mark.asyncio + async def test_single_fixture_prediction_shows_saved_scores( + self, user_commands, mock_interaction, database, sample_games + ): + fixture_id = await database.create_fixture( + 1, sample_games, datetime.now(UTC) + timedelta(days=1) + ) + await database.save_prediction( + fixture_id, + str(mock_interaction.user.id), + mock_interaction.user.name, + ["2-1", "1-1", "0-2"], + False, + ) + + await user_commands.my_predictions.callback(user_commands, mock_interaction) + + content = mock_interaction.response_sent[0]["content"] + assert "Your Predictions:" in content + assert f"1. {sample_games[0]} **2-1**" in content + assert "Status:" in content + assert "Submitted:" in content + + @pytest.mark.asyncio + async def test_multiple_open_fixtures_show_mixed_prediction_state( + self, user_commands, mock_interaction, database, sample_games + ): + deadline = datetime.now(UTC) + timedelta(days=1) + fixture_week_1 = await database.create_fixture(1, sample_games, deadline) + await database.create_fixture(2, sample_games, deadline) + await database.save_prediction( + fixture_week_1, + str(mock_interaction.user.id), + mock_interaction.user.name, + ["2-1", "1-1", "0-2"], + False, + ) + + await user_commands.my_predictions.callback(user_commands, mock_interaction) + + content = mock_interaction.response_sent[0]["content"] + assert "Your Predictions (Open Fixtures):" in content + assert "Week 1" in content + assert "Week 2" in content + assert f"1. {sample_games[0]} **2-1**" in content + assert "No prediction submitted yet." in content