+
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
5 changes: 5 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
DISCORD_TOKEN="abcdefghijklmnopqrstuvwxyz"
DISCORD_GUILD="Test Guild"
DATABASE_URL="sqlite+aiosqlite:///:memory:"
MEMBER_ROLE_NAME="members"
COC_MESSAGE_ID="123456789"
28 changes: 28 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Run Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Install the latest version of uv
uses: astral-sh/setup-uv@0c5e2b8115b80b4c7c5ddf6ffdd634974642d182 # v5.4.1
with:
python-version: 3.12

- name: Run tests
run: |
uv run pytest --cov --cov-report=xml

- name: Upload results to Codecov
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0
with:
token: ${{ secrets.CODECOV_TOKEN }}
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,6 @@ dmypy.json

# VS Code
.vscode/

# macOS
.DS_Store
6 changes: 6 additions & 0 deletions bot/bot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Any

import discord
from discord.ext.commands import Bot
Expand Down Expand Up @@ -30,6 +31,11 @@ class PyGreeceBot(Bot):
sending welcome messages, and assigning roles based on reactions.
"""

@Bot.user.setter # type: ignore
def _user(self, value: Any) -> None:
"""Set the user attribute. Only used in tests for mocking the user"""
self._connection.user = value

async def on_ready(self) -> None:
"""Called when the bot is ready and has logged in."""
assert self.user is not None
Expand Down
22 changes: 21 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ email = "organizers@pygreece.org"

[dependency-groups]
dev = [
"aiosqlite>=0.21.0",
"mypy>=1.15.0",
"pre-commit>=4.1.0",
"pytest>=8.3.5",
"pytest-asyncio>=0.26.0",
"pytest-cov>=6.0.0",
"pytest-dotenv>=0.5.2",
"ruff>=0.11.0",
]

Expand All @@ -31,5 +36,20 @@ select = ["E4", "E7", "E9", "F", "I"]

[tool.mypy]
python_version = "3.12"
files = ["bot"]
files = ["bot", "tests"]
strict = true

[[tool.mypy.overrides]]
module = 'tests.*'
disable_error_code = ["method-assign"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
asyncio_default_fixture_loop_scope = "function"
asyncio_default_test_loop_scope = "function"
env_override_existing_values = 1
env_files = [".env.test"]

[tool.coverage.run]
branch = true
source = ["bot", "tests"]
Empty file added tests/__init__.py
Empty file.
120 changes: 120 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from contextlib import asynccontextmanager
from typing import AsyncGenerator
from unittest.mock import AsyncMock, MagicMock

import discord
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import (
AsyncEngine,
AsyncSession,
async_sessionmaker,
create_async_engine,
)

from bot import config
from bot.models import Base


@pytest_asyncio.fixture(loop_scope="session", scope="session")
async def test_engine() -> AsyncGenerator[AsyncEngine, None]:
"""Create a test engine for the test session."""
engine = create_async_engine(config.DATABASE_URL)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)

yield engine

async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)

await engine.dispose()


@pytest.fixture
async def test_session(test_engine: AsyncEngine) -> AsyncGenerator[AsyncSession, None]:
"""Create a test session for each test."""
connection = await test_engine.connect()
transaction = await connection.begin()

async_session_factory = async_sessionmaker(
connection, class_=AsyncSession, expire_on_commit=False
)
session = async_session_factory()

yield session

await session.close()
await transaction.rollback()
await connection.close()


@pytest.fixture
def mock_session(test_session: AsyncSession, monkeypatch: pytest.MonkeyPatch) -> AsyncSession:
"""Mock the db_session context manager to return the test session."""

@asynccontextmanager
async def mock_get_session() -> AsyncGenerator[AsyncSession, None]:
yield test_session

# Patch the db_session function
monkeypatch.setattr("bot.db.get_session", mock_get_session)

return test_session


@pytest.fixture
def mock_discord_user() -> MagicMock:
"""Create a mock Discord user."""
user = MagicMock(spec=discord.User)
user.id = 123456789
user.name = "TestUser"
user.bot = False
return user


@pytest.fixture
def mock_discord_role() -> MagicMock:
"""Create a mock Discord role."""
role = MagicMock(spec=discord.Role)
role.name = "members"
role.id = 987654321
return role


@pytest.fixture
def mock_discord_guild(mock_discord_role: MagicMock) -> MagicMock:
"""Create a mock Discord guild."""
guild = MagicMock(spec=discord.Guild)
guild.name = config.DISCORD_GUILD
guild.id = 111222333
guild.roles = [mock_discord_role]
return guild


@pytest.fixture
def mock_discord_member(mock_discord_user: MagicMock, mock_discord_guild: MagicMock) -> MagicMock:
"""Create a mock Discord member."""
member = MagicMock(spec=discord.Member)
member.id = mock_discord_user.id
member.name = mock_discord_user.name
member.bot = False
member.guild = mock_discord_guild
member.roles = []
member.add_roles = AsyncMock()
member.send = AsyncMock()
return member


@pytest.fixture
def mock_reaction_payload(
mock_discord_user: MagicMock, mock_discord_guild: MagicMock
) -> MagicMock:
"""Create a mock reaction payload."""
payload = MagicMock(spec=discord.RawReactionActionEvent)
payload.user_id = mock_discord_user.id
payload.message_id = 123456789
payload.guild_id = mock_discord_guild.id
payload.emoji = MagicMock(spec=discord.Emoji)
payload.emoji.name = "✅"
return payload
52 changes: 52 additions & 0 deletions tests/test___main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from typing import Generator
from unittest.mock import MagicMock, patch

import pytest


@pytest.fixture
def mock_intents() -> Generator[tuple[MagicMock, MagicMock], None, None]:
"""Create a mock for discord.Intents.default()"""
with patch("discord.Intents.default") as mock_intents_default:
mock_intents = MagicMock()
mock_intents_default.return_value = mock_intents
yield mock_intents, mock_intents_default


@pytest.fixture
def mock_bot() -> Generator[tuple[MagicMock, MagicMock], None, None]:
"""Create a mock for the PyGreeceBot class"""
with patch("bot.bot.PyGreeceBot") as mock_bot_class:
mock_bot_instance = MagicMock()
mock_bot_class.return_value = mock_bot_instance
yield mock_bot_instance, mock_bot_class


@patch("discord.utils.setup_logging")
@patch("bot.config.DISCORD_TOKEN", "mock_token")
def test_bot_initialization(
mock_setup_logging: MagicMock,
mock_bot: tuple[MagicMock, MagicMock],
mock_intents: tuple[MagicMock, MagicMock],
) -> None:
"""Test that the bot is initialized with the correct parameters"""
mock_bot_instance, mock_bot_class = mock_bot
mock_intents_instance, mock_intents_default = mock_intents

import bot.__main__ # noqa: F401

# Verify Discord logging was set up
mock_setup_logging.assert_called_once()

# Verify intents were properly configured
mock_intents_default.assert_called_once()
assert mock_intents_instance.members

# Verify bot was initialized correctly
mock_bot_class.assert_called_once()
_, kwargs = mock_bot_class.call_args
assert kwargs["command_prefix"] == "!"
assert kwargs["intents"] == mock_intents_instance

# Verify bot.run was called with the token
mock_bot_instance.run.assert_called_once_with("mock_token")
Loading
点击 这是indexloc提供的php浏览器服务,不要输入任何密码和下载