242 lines
9.0 KiB
Python
242 lines
9.0 KiB
Python
"""Tests for the DevToolsCog /dev refractor-test command."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import discord
|
|
import pytest
|
|
from discord.ext import commands
|
|
|
|
|
|
class TestCleanupView:
|
|
"""Test the cleanup button view for the refractor integration test.
|
|
|
|
The view presents two buttons after a test run: 'Clean Up Test Data'
|
|
deletes the synthetic game/plays/decisions; 'Keep Test Data' dismisses
|
|
the buttons. Only the command invoker can press them.
|
|
"""
|
|
|
|
@pytest.fixture
|
|
def owner_interaction(self):
|
|
"""Interaction from the user who ran the command."""
|
|
interaction = AsyncMock(spec=discord.Interaction)
|
|
interaction.user = MagicMock()
|
|
interaction.user.id = 12345
|
|
interaction.response = AsyncMock()
|
|
return interaction
|
|
|
|
@pytest.fixture
|
|
def other_interaction(self):
|
|
"""Interaction from a different user — should be rejected."""
|
|
interaction = AsyncMock(spec=discord.Interaction)
|
|
interaction.user = MagicMock()
|
|
interaction.user.id = 99999
|
|
interaction.response = AsyncMock()
|
|
return interaction
|
|
|
|
async def test_view_has_two_buttons(self):
|
|
"""View should have exactly two buttons: cleanup and keep.
|
|
|
|
Must be async because discord.ui.View.__init__ calls
|
|
asyncio.get_running_loop() internally and requires an event loop.
|
|
"""
|
|
from cogs.dev_tools import CleanupView
|
|
|
|
view = CleanupView(owner_id=12345, game_id=1, embed=discord.Embed())
|
|
buttons = [
|
|
child for child in view.children if isinstance(child, discord.ui.Button)
|
|
]
|
|
assert len(buttons) == 2
|
|
|
|
async def test_unauthorized_user_ignored(self, other_interaction):
|
|
"""Non-owner clicks should be silently ignored."""
|
|
from cogs.dev_tools import CleanupView
|
|
|
|
view = CleanupView(owner_id=12345, game_id=1, embed=discord.Embed())
|
|
with patch("cogs.dev_tools.db_delete", new_callable=AsyncMock) as mock_delete:
|
|
await view.cleanup_btn.callback(other_interaction)
|
|
mock_delete.assert_not_called()
|
|
other_interaction.response.edit_message.assert_not_called()
|
|
|
|
async def test_cleanup_calls_delete_endpoints(self, owner_interaction):
|
|
"""Cleanup button deletes decisions, plays, then game in order."""
|
|
from cogs.dev_tools import CleanupView
|
|
|
|
embed = discord.Embed(description="test")
|
|
view = CleanupView(owner_id=12345, game_id=42, embed=embed)
|
|
with patch("cogs.dev_tools.db_delete", new_callable=AsyncMock) as mock_delete:
|
|
await view.cleanup_btn.callback(owner_interaction)
|
|
assert mock_delete.call_count == 3
|
|
# Verify correct endpoints and order
|
|
calls = mock_delete.call_args_list
|
|
assert "decisions/game" in str(calls[0])
|
|
assert "plays/game" in str(calls[1])
|
|
assert "games" in str(calls[2])
|
|
|
|
async def test_keep_removes_buttons(self, owner_interaction):
|
|
"""Keep button removes buttons and updates embed."""
|
|
from cogs.dev_tools import CleanupView
|
|
|
|
embed = discord.Embed(description="test")
|
|
view = CleanupView(owner_id=12345, game_id=42, embed=embed)
|
|
await view.keep_btn.callback(owner_interaction)
|
|
owner_interaction.response.edit_message.assert_called_once()
|
|
|
|
|
|
class TestRefractorTestSetup:
|
|
"""Test the setup phase: card lookup, refractor state, plan calculation."""
|
|
|
|
@pytest.fixture
|
|
def mock_interaction(self):
|
|
interaction = AsyncMock(spec=discord.Interaction)
|
|
interaction.user = MagicMock()
|
|
interaction.user.id = 12345
|
|
interaction.response = AsyncMock()
|
|
interaction.edit_original_response = AsyncMock()
|
|
interaction.followup = AsyncMock()
|
|
return interaction
|
|
|
|
@pytest.fixture
|
|
def mock_bot(self):
|
|
return MagicMock(spec=commands.Bot)
|
|
|
|
@pytest.fixture
|
|
def batting_card_response(self):
|
|
return {
|
|
"id": 1234,
|
|
"player": {"id": 100, "p_name": "Mike Trout"},
|
|
"variant": 0,
|
|
"image_url": None,
|
|
}
|
|
|
|
@pytest.fixture
|
|
def refractor_cards_response(self):
|
|
return {
|
|
"count": 1,
|
|
"items": [
|
|
{
|
|
"player_id": 100,
|
|
"team_id": 31,
|
|
"current_tier": 0,
|
|
"current_value": 30.0,
|
|
"fully_evolved": False,
|
|
"track": {
|
|
"card_type": "batter",
|
|
"t1_threshold": 37,
|
|
"t2_threshold": 149,
|
|
"t3_threshold": 448,
|
|
"t4_threshold": 896,
|
|
},
|
|
"next_threshold": 37,
|
|
"progress_pct": 81.1,
|
|
"player_name": "Mike Trout",
|
|
"image_url": None,
|
|
}
|
|
],
|
|
}
|
|
|
|
@pytest.fixture
|
|
def opposing_cards_response(self):
|
|
"""A valid pitching cards response with the 'cards' key."""
|
|
return {
|
|
"cards": [
|
|
{
|
|
"id": 9000,
|
|
"player": {"id": 200, "p_name": "Clayton Kershaw"},
|
|
"variant": 0,
|
|
}
|
|
]
|
|
}
|
|
|
|
async def test_batting_card_lookup(
|
|
self,
|
|
mock_interaction,
|
|
mock_bot,
|
|
batting_card_response,
|
|
refractor_cards_response,
|
|
opposing_cards_response,
|
|
):
|
|
"""Command should try the batting card endpoint first.
|
|
|
|
Verifies that the first db_get call targets 'battingcards', not
|
|
'pitchingcards', when looking up a card ID.
|
|
"""
|
|
from cogs.dev_tools import DevToolsCog
|
|
|
|
cog = DevToolsCog(mock_bot)
|
|
with (
|
|
patch("cogs.dev_tools.db_get", new_callable=AsyncMock) as mock_get,
|
|
patch("cogs.dev_tools.db_post", new_callable=AsyncMock),
|
|
):
|
|
mock_get.side_effect = [
|
|
batting_card_response, # GET battingcards/{id}
|
|
refractor_cards_response, # GET refractor/cards
|
|
opposing_cards_response, # GET pitchingcards (for opposing player)
|
|
]
|
|
with patch.object(cog, "_execute_refractor_test", new_callable=AsyncMock):
|
|
await cog.refractor_test.callback(cog, mock_interaction, card_id=1234)
|
|
first_call = mock_get.call_args_list[0]
|
|
assert "battingcards" in str(first_call)
|
|
|
|
async def test_pitching_card_fallback(
|
|
self,
|
|
mock_interaction,
|
|
mock_bot,
|
|
refractor_cards_response,
|
|
):
|
|
"""If batting card returns None, command should fall back to pitching card.
|
|
|
|
Ensures the two-step lookup: batting first, then pitching if batting
|
|
returns None. The second db_get call must target 'pitchingcards'.
|
|
"""
|
|
from cogs.dev_tools import DevToolsCog
|
|
|
|
cog = DevToolsCog(mock_bot)
|
|
pitching_card = {
|
|
"id": 5678,
|
|
"player": {"id": 200, "p_name": "Clayton Kershaw"},
|
|
"variant": 0,
|
|
"image_url": None,
|
|
}
|
|
refractor_cards_response["items"][0]["player_id"] = 200
|
|
refractor_cards_response["items"][0]["track"]["card_type"] = "sp"
|
|
refractor_cards_response["items"][0]["next_threshold"] = 10
|
|
|
|
opposing_batters = {
|
|
"cards": [
|
|
{"id": 7000, "player": {"id": 300, "p_name": "Babe Ruth"}, "variant": 0}
|
|
]
|
|
}
|
|
|
|
with (
|
|
patch("cogs.dev_tools.db_get", new_callable=AsyncMock) as mock_get,
|
|
patch("cogs.dev_tools.db_post", new_callable=AsyncMock),
|
|
):
|
|
mock_get.side_effect = [
|
|
None, # batting card not found
|
|
pitching_card, # pitching card found
|
|
refractor_cards_response, # refractor/cards
|
|
opposing_batters, # battingcards for opposing player
|
|
]
|
|
with patch.object(cog, "_execute_refractor_test", new_callable=AsyncMock):
|
|
await cog.refractor_test.callback(cog, mock_interaction, card_id=5678)
|
|
second_call = mock_get.call_args_list[1]
|
|
assert "pitchingcards" in str(second_call)
|
|
|
|
async def test_card_not_found_reports_error(self, mock_interaction, mock_bot):
|
|
"""If neither batting nor pitching card exists, report an error and return.
|
|
|
|
The command should call edit_original_response with a message containing
|
|
'not found' and must NOT call _execute_refractor_test.
|
|
"""
|
|
from cogs.dev_tools import DevToolsCog
|
|
|
|
cog = DevToolsCog(mock_bot)
|
|
with patch("cogs.dev_tools.db_get", new_callable=AsyncMock, return_value=None):
|
|
with patch.object(
|
|
cog, "_execute_refractor_test", new_callable=AsyncMock
|
|
) as mock_exec:
|
|
await cog.refractor_test.callback(cog, mock_interaction, card_id=9999)
|
|
mock_exec.assert_not_called()
|
|
call_kwargs = mock_interaction.edit_original_response.call_args[1]
|
|
assert "not found" in call_kwargs["content"].lower()
|