feat: add CleanupView for refractor test data
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0aeed0c76f
commit
777e6de3de
77
cogs/dev_tools.py
Normal file
77
cogs/dev_tools.py
Normal file
@ -0,0 +1,77 @@
|
||||
"""Dev-only tools for testing Paper Dynasty systems.
|
||||
|
||||
This cog is only loaded when DATABASE != prod. It provides commands
|
||||
for integration testing that create and clean up synthetic test data.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
from api_calls import db_delete
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CleanupView(discord.ui.View):
|
||||
"""Post-test buttons to clean up or keep synthetic game data."""
|
||||
|
||||
def __init__(
|
||||
self, owner_id: int, game_id: int, embed: discord.Embed, timeout: float = 300.0
|
||||
):
|
||||
super().__init__(timeout=timeout)
|
||||
self.owner_id = owner_id
|
||||
self.game_id = game_id
|
||||
self.embed = embed
|
||||
|
||||
@discord.ui.button(
|
||||
label="Clean Up Test Data", style=discord.ButtonStyle.danger, emoji="🧹"
|
||||
)
|
||||
async def cleanup_btn(
|
||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||
):
|
||||
if interaction.user.id != self.owner_id:
|
||||
return
|
||||
|
||||
try:
|
||||
await db_delete("decisions/game", self.game_id)
|
||||
await db_delete("plays/game", self.game_id)
|
||||
await db_delete("games", self.game_id)
|
||||
self.embed.add_field(
|
||||
name="",
|
||||
value=f"🧹 Test data cleaned up (game #{self.game_id} removed)",
|
||||
inline=False,
|
||||
)
|
||||
except Exception as e:
|
||||
self.embed.add_field(
|
||||
name="",
|
||||
value=f"❌ Cleanup failed: {e}",
|
||||
inline=False,
|
||||
)
|
||||
|
||||
self.clear_items()
|
||||
await interaction.response.edit_message(embed=self.embed, view=self)
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(
|
||||
label="Keep Test Data", style=discord.ButtonStyle.secondary, emoji="📌"
|
||||
)
|
||||
async def keep_btn(
|
||||
self, interaction: discord.Interaction, button: discord.ui.Button
|
||||
):
|
||||
if interaction.user.id != self.owner_id:
|
||||
return
|
||||
|
||||
self.embed.add_field(
|
||||
name="",
|
||||
value=f"📌 Test data kept (game #{self.game_id})",
|
||||
inline=False,
|
||||
)
|
||||
self.clear_items()
|
||||
await interaction.response.edit_message(embed=self.embed, view=self)
|
||||
self.stop()
|
||||
|
||||
async def on_timeout(self):
|
||||
self.clear_items()
|
||||
81
tests/test_dev_tools.py
Normal file
81
tests/test_dev_tools.py
Normal file
@ -0,0 +1,81 @@
|
||||
"""Tests for the DevToolsCog /dev refractor-test command."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import discord
|
||||
import pytest
|
||||
|
||||
|
||||
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()
|
||||
Loading…
Reference in New Issue
Block a user