|
| 1 | +"""Test fixtures for bot service testing.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +from typing import TYPE_CHECKING |
| 6 | +from unittest.mock import AsyncMock, MagicMock |
| 7 | +from uuid import uuid4 |
| 8 | + |
| 9 | +import pytest |
| 10 | +import respx |
| 11 | +from httpx import Response |
| 12 | + |
| 13 | +if TYPE_CHECKING: |
| 14 | + from discord import Guild, Interaction, Member, Message, Role, TextChannel, User |
| 15 | + from discord.ext.commands import Bot, Context |
| 16 | + |
| 17 | +__all__ = ( |
| 18 | + "mock_api_client", |
| 19 | + "mock_api_responses", |
| 20 | + "mock_bot", |
| 21 | + "mock_channel", |
| 22 | + "mock_context", |
| 23 | + "mock_guild", |
| 24 | + "mock_interaction", |
| 25 | + "mock_member", |
| 26 | + "mock_message", |
| 27 | + "mock_role", |
| 28 | + "mock_user", |
| 29 | +) |
| 30 | + |
| 31 | + |
| 32 | +@pytest.fixture |
| 33 | +def mock_bot() -> Bot: |
| 34 | + """Create a mock discord.ext.commands.Bot. |
| 35 | +
|
| 36 | + Returns: |
| 37 | + Mock bot instance with common attributes. |
| 38 | + """ |
| 39 | + bot = MagicMock() |
| 40 | + bot.user = MagicMock() |
| 41 | + bot.user.id = 123456789 |
| 42 | + bot.user.name = "ByteBot" |
| 43 | + bot.user.discriminator = "0000" |
| 44 | + bot.is_owner = AsyncMock(return_value=False) |
| 45 | + bot.load_extension = AsyncMock() |
| 46 | + bot.tree = MagicMock() |
| 47 | + bot.tree.sync = AsyncMock() |
| 48 | + bot.tree.copy_global_to = MagicMock() |
| 49 | + return bot |
| 50 | + |
| 51 | + |
| 52 | +@pytest.fixture |
| 53 | +def mock_user() -> User: |
| 54 | + """Create a mock discord.User. |
| 55 | +
|
| 56 | + Returns: |
| 57 | + Mock user instance. |
| 58 | + """ |
| 59 | + user = MagicMock() |
| 60 | + user.id = 987654321 |
| 61 | + user.name = "TestUser" |
| 62 | + user.discriminator = "1234" |
| 63 | + user.mention = "<@987654321>" |
| 64 | + user.avatar = MagicMock() |
| 65 | + user.avatar.url = "https://cdn.discordapp.com/avatars/987654321/test.png" |
| 66 | + user.bot = False |
| 67 | + return user |
| 68 | + |
| 69 | + |
| 70 | +@pytest.fixture |
| 71 | +def mock_role() -> Role: |
| 72 | + """Create a mock discord.Role. |
| 73 | +
|
| 74 | + Returns: |
| 75 | + Mock role instance. |
| 76 | + """ |
| 77 | + role = MagicMock() |
| 78 | + role.id = 111111111 |
| 79 | + role.name = "TestRole" |
| 80 | + role.mention = "<@&111111111>" |
| 81 | + return role |
| 82 | + |
| 83 | + |
| 84 | +@pytest.fixture |
| 85 | +def mock_member(mock_user: User, mock_role: Role) -> Member: |
| 86 | + """Create a mock discord.Member. |
| 87 | +
|
| 88 | + Args: |
| 89 | + mock_user: Mock user fixture |
| 90 | + mock_role: Mock role fixture |
| 91 | +
|
| 92 | + Returns: |
| 93 | + Mock member instance. |
| 94 | + """ |
| 95 | + member = MagicMock() |
| 96 | + member.id = mock_user.id |
| 97 | + member.name = mock_user.name |
| 98 | + member.discriminator = mock_user.discriminator |
| 99 | + member.mention = mock_user.mention |
| 100 | + member.avatar = mock_user.avatar |
| 101 | + member.bot = False |
| 102 | + member.roles = [mock_role] |
| 103 | + member.guild_permissions = MagicMock() |
| 104 | + member.guild_permissions.administrator = False |
| 105 | + member.send = AsyncMock() |
| 106 | + return member |
| 107 | + |
| 108 | + |
| 109 | +@pytest.fixture |
| 110 | +def mock_guild(mock_member: Member) -> Guild: |
| 111 | + """Create a mock discord.Guild. |
| 112 | +
|
| 113 | + Args: |
| 114 | + mock_member: Mock member fixture |
| 115 | +
|
| 116 | + Returns: |
| 117 | + Mock guild instance. |
| 118 | + """ |
| 119 | + guild = MagicMock() |
| 120 | + guild.id = 555555555 |
| 121 | + guild.name = "Test Guild" |
| 122 | + guild.get_member = MagicMock(return_value=mock_member) |
| 123 | + guild.owner_id = 999999999 |
| 124 | + return guild |
| 125 | + |
| 126 | + |
| 127 | +@pytest.fixture |
| 128 | +def mock_channel() -> TextChannel: |
| 129 | + """Create a mock discord.TextChannel. |
| 130 | +
|
| 131 | + Returns: |
| 132 | + Mock text channel instance. |
| 133 | + """ |
| 134 | + channel = MagicMock() |
| 135 | + channel.id = 777777777 |
| 136 | + channel.name = "test-channel" |
| 137 | + channel.mention = "<#777777777>" |
| 138 | + channel.send = AsyncMock() |
| 139 | + return channel |
| 140 | + |
| 141 | + |
| 142 | +@pytest.fixture |
| 143 | +def mock_message(mock_user: User, mock_channel: TextChannel, mock_guild: Guild) -> Message: |
| 144 | + """Create a mock discord.Message. |
| 145 | +
|
| 146 | + Args: |
| 147 | + mock_user: Mock user fixture |
| 148 | + mock_channel: Mock channel fixture |
| 149 | + mock_guild: Mock guild fixture |
| 150 | +
|
| 151 | + Returns: |
| 152 | + Mock message instance. |
| 153 | + """ |
| 154 | + message = MagicMock() |
| 155 | + message.id = 888888888 |
| 156 | + message.author = mock_user |
| 157 | + message.channel = mock_channel |
| 158 | + message.guild = mock_guild |
| 159 | + message.content = "!test command" |
| 160 | + message.jump_url = f"https://discord.com/channels/{mock_guild.id}/{mock_channel.id}/{message.id}" |
| 161 | + message.created_at = MagicMock() |
| 162 | + message.created_at.strftime = MagicMock(return_value="2024-01-01 12:00:00") |
| 163 | + message.delete = AsyncMock() |
| 164 | + return message |
| 165 | + |
| 166 | + |
| 167 | +@pytest.fixture |
| 168 | +def mock_interaction(mock_user: User, mock_guild: Guild, mock_channel: TextChannel) -> Interaction: |
| 169 | + """Create a mock discord.Interaction. |
| 170 | +
|
| 171 | + Args: |
| 172 | + mock_user: Mock user fixture |
| 173 | + mock_guild: Mock guild fixture |
| 174 | + mock_channel: Mock channel fixture |
| 175 | +
|
| 176 | + Returns: |
| 177 | + Mock interaction instance. |
| 178 | + """ |
| 179 | + interaction = MagicMock() |
| 180 | + interaction.user = mock_user |
| 181 | + interaction.guild = mock_guild |
| 182 | + interaction.channel = mock_channel |
| 183 | + interaction.message = None |
| 184 | + interaction.response = MagicMock() |
| 185 | + interaction.response.send_message = AsyncMock() |
| 186 | + interaction.response.defer = AsyncMock() |
| 187 | + return interaction |
| 188 | + |
| 189 | + |
| 190 | +@pytest.fixture |
| 191 | +def mock_context( |
| 192 | + mock_bot: Bot, |
| 193 | + mock_user: User, |
| 194 | + mock_guild: Guild, |
| 195 | + mock_channel: TextChannel, |
| 196 | + mock_message: Message, |
| 197 | +) -> Context: |
| 198 | + """Create a mock discord.ext.commands.Context. |
| 199 | +
|
| 200 | + Args: |
| 201 | + mock_bot: Mock bot fixture |
| 202 | + mock_user: Mock user fixture |
| 203 | + mock_guild: Mock guild fixture |
| 204 | + mock_channel: Mock channel fixture |
| 205 | + mock_message: Mock message fixture |
| 206 | +
|
| 207 | + Returns: |
| 208 | + Mock context instance. |
| 209 | + """ |
| 210 | + context = MagicMock() |
| 211 | + context.bot = mock_bot |
| 212 | + context.author = mock_user |
| 213 | + context.guild = mock_guild |
| 214 | + context.channel = mock_channel |
| 215 | + context.message = mock_message |
| 216 | + context.command = MagicMock() |
| 217 | + context.command.name = "test" |
| 218 | + context.interaction = None |
| 219 | + context.send = AsyncMock() |
| 220 | + return context |
| 221 | + |
| 222 | + |
| 223 | +@pytest.fixture |
| 224 | +def mock_api_responses() -> dict[str, dict]: |
| 225 | + """Mock API response data. |
| 226 | +
|
| 227 | + Returns: |
| 228 | + Dictionary of mock API responses. |
| 229 | + """ |
| 230 | + guild_id = 555555555 |
| 231 | + guild_uuid = str(uuid4()) |
| 232 | + |
| 233 | + return { |
| 234 | + "guild_created": { |
| 235 | + "id": guild_uuid, |
| 236 | + "guild_id": guild_id, |
| 237 | + "guild_name": "Test Guild", |
| 238 | + "prefix": "!", |
| 239 | + "created_at": "2024-01-01T12:00:00Z", |
| 240 | + "updated_at": "2024-01-01T12:00:00Z", |
| 241 | + }, |
| 242 | + "guild_existing": { |
| 243 | + "id": guild_uuid, |
| 244 | + "guild_id": guild_id, |
| 245 | + "guild_name": "Existing Guild", |
| 246 | + "prefix": "?", |
| 247 | + "created_at": "2024-01-01T00:00:00Z", |
| 248 | + "updated_at": "2024-01-01T00:00:00Z", |
| 249 | + }, |
| 250 | + "guild_updated": { |
| 251 | + "id": guild_uuid, |
| 252 | + "guild_id": guild_id, |
| 253 | + "guild_name": "Test Guild", |
| 254 | + "prefix": ">>", |
| 255 | + "created_at": "2024-01-01T12:00:00Z", |
| 256 | + "updated_at": "2024-01-01T13:00:00Z", |
| 257 | + }, |
| 258 | + "health_check": { |
| 259 | + "status": "healthy", |
| 260 | + "database": "connected", |
| 261 | + "timestamp": "2024-01-01T12:00:00Z", |
| 262 | + }, |
| 263 | + "validation_error": { |
| 264 | + "detail": [ |
| 265 | + { |
| 266 | + "type": "string_too_short", |
| 267 | + "loc": ["body", "guild_name"], |
| 268 | + "msg": "String should have at least 1 character", |
| 269 | + } |
| 270 | + ] |
| 271 | + }, |
| 272 | + "server_error": {"detail": "Internal server error"}, |
| 273 | + } |
| 274 | + |
| 275 | + |
| 276 | +@pytest.fixture |
| 277 | +def mock_api_client(respx_mock: respx.MockRouter, mock_api_responses: dict[str, dict]) -> respx.MockRouter: |
| 278 | + """Mock ByteAPIClient HTTP calls using respx. |
| 279 | +
|
| 280 | + Args: |
| 281 | + respx_mock: respx mock router |
| 282 | + mock_api_responses: Mock API response data |
| 283 | +
|
| 284 | + Returns: |
| 285 | + Configured respx mock router. |
| 286 | + """ |
| 287 | + base_url = "http://localhost:8000" |
| 288 | + |
| 289 | + # Health check endpoint |
| 290 | + respx_mock.get(f"{base_url}/health").mock(return_value=Response(200, json=mock_api_responses["health_check"])) |
| 291 | + |
| 292 | + # Guild endpoints - GET by ID (success) |
| 293 | + respx_mock.get(f"{base_url}/api/guilds/555555555").mock( |
| 294 | + return_value=Response(200, json=mock_api_responses["guild_existing"]) |
| 295 | + ) |
| 296 | + |
| 297 | + # Guild endpoints - GET by ID (not found) |
| 298 | + respx_mock.get(f"{base_url}/api/guilds/999999999").mock(return_value=Response(404, json={"detail": "Not found"})) |
| 299 | + |
| 300 | + # Guild endpoints - POST (create) |
| 301 | + respx_mock.post(f"{base_url}/api/guilds").mock(return_value=Response(201, json=mock_api_responses["guild_created"])) |
| 302 | + |
| 303 | + return respx_mock |
0 commit comments