Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions tests/test_admin_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down
149 changes: 149 additions & 0 deletions tests/test_user_commands.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Loading