diff --git a/.gitignore b/.gitignore index b476fd9..7602f82 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,70 @@ +# IDE and cache .cache .idea +.vscode +*__pycache__* +*.pyc +*.pyo +*.pyd +.Python + +# Environment .env* +venv/ +.venv/ +env/ +ENV/ + +# Logs log test/log test/**/log -.pytest_cache -.vscode -*__pycache__* +*.log + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +coverage.xml +*.cover +.hypothesis/ +.tox/ + +# Claude +.claude/* + +# Build artifacts +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Poetry +poetry.lock + +# Data data test/data test/**/data -# pipenv +# pipenv (legacy) Pipfile Pipfile.lock + +# OS files +.DS_Store +Thumbs.db +*.swp +*.swo +*~ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..06258a8 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,101 @@ +[tool.poetry] +name = "midjourney-api" +version = "0.1.0" +description = "Midjourney API integration project" +authors = ["Your Name "] +readme = "README.md" +packages = [{include = "app"}, {include = "lib"}, {include = "task"}, {include = "util"}] + +[tool.poetry.dependencies] +python = "^3.8" +discord-py = "2.2.3" +fastapi = "0.95" +python-dotenv = "1.0.0" +uvicorn = "0.22.0" +pydantic = "~1.10.7" +aiohttp = "~3.8.4" +loguru = "0.7.0" +aiofiles = "23.1.0" +python-multipart = "0.0.6" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.0" +pytest-cov = "^4.1.0" +pytest-mock = "^3.11.1" +pytest-asyncio = "^0.21.1" + +[tool.poetry.scripts] +test = "pytest:main" +tests = "pytest:main" + +[tool.pytest.ini_options] +minversion = "7.0" +testpaths = ["tests"] +python_files = ["test_*.py", "*_test.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = [ + "--verbose", + "--strict-markers", + "--cov=app", + "--cov=lib", + "--cov=task", + "--cov=util", + "--cov-report=html", + "--cov-report=xml", + "--cov-report=term-missing", + "--cov-fail-under=80", +] +markers = [ + "unit: Unit tests", + "integration: Integration tests", + "slow: Slow running tests", +] +filterwarnings = [ + "error", + "ignore::UserWarning", + "ignore::DeprecationWarning", +] + +[tool.coverage.run] +source = ["app", "lib", "task", "util"] +omit = [ + "*/tests/*", + "*/__pycache__/*", + "*/.venv/*", + "*/venv/*", + "*/.tox/*", + "*/.coverage*", + "*/htmlcov/*", + "*/dist/*", + "*/build/*", + "*/.pytest_cache/*", + "*/__init__.py", +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if self.debug:", + "if __name__ == .__main__.:", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if False:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] +precision = 2 +show_missing = true +skip_covered = false + +[tool.coverage.html] +directory = "htmlcov" + +[tool.coverage.xml] +output = "coverage.xml" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b23a528 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,180 @@ +"""Shared pytest fixtures for testing.""" +import os +import tempfile +from pathlib import Path +from typing import Generator, Dict, Any +import pytest +from unittest.mock import Mock, MagicMock + + +@pytest.fixture +def temp_dir() -> Generator[Path, None, None]: + """Create a temporary directory for testing. + + Yields: + Path: Path to the temporary directory + """ + with tempfile.TemporaryDirectory() as tmpdir: + yield Path(tmpdir) + + +@pytest.fixture +def mock_env_vars(monkeypatch) -> Dict[str, str]: + """Mock environment variables for testing. + + Returns: + Dict[str, str]: Dictionary of test environment variables + """ + test_env = { + "DISCORD_TOKEN": "test_discord_token", + "API_KEY": "test_api_key", + "DEBUG": "true", + "LOG_LEVEL": "DEBUG", + "PORT": "8000", + } + + for key, value in test_env.items(): + monkeypatch.setenv(key, value) + + return test_env + + +@pytest.fixture +def mock_config() -> Dict[str, Any]: + """Provide mock configuration for testing. + + Returns: + Dict[str, Any]: Test configuration dictionary + """ + return { + "app": { + "name": "test_app", + "version": "0.1.0", + "debug": True, + }, + "discord": { + "token": "test_token", + "guild_id": "123456789", + "channel_id": "987654321", + }, + "api": { + "base_url": "http://localhost:8000", + "timeout": 30, + "retry_attempts": 3, + }, + } + + +@pytest.fixture +def mock_discord_client(): + """Create a mock Discord client for testing. + + Returns: + Mock: Mocked Discord client + """ + client = MagicMock() + client.user = MagicMock() + client.user.id = 123456789 + client.user.name = "TestBot" + client.is_ready.return_value = True + return client + + +@pytest.fixture +def mock_fastapi_client(): + """Create a mock FastAPI test client. + + Returns: + Mock: Mocked FastAPI test client + """ + client = MagicMock() + client.base_url = "http://testserver" + return client + + +@pytest.fixture +def sample_discord_message(): + """Create a sample Discord message for testing. + + Returns: + Dict[str, Any]: Sample Discord message data + """ + return { + "id": "1234567890", + "content": "Test message content", + "author": { + "id": "987654321", + "username": "testuser", + "discriminator": "1234", + }, + "channel_id": "111222333", + "guild_id": "444555666", + "timestamp": "2023-01-01T00:00:00.000Z", + } + + +@pytest.fixture +def sample_api_response(): + """Create a sample API response for testing. + + Returns: + Dict[str, Any]: Sample API response data + """ + return { + "status": "success", + "data": { + "id": "response_123", + "result": "Test result", + "metadata": { + "timestamp": "2023-01-01T00:00:00.000Z", + "version": "1.0.0", + }, + }, + "error": None, + } + + +@pytest.fixture(autouse=True) +def reset_environment(monkeypatch): + """Reset environment variables before each test. + + This fixture runs automatically before each test to ensure + a clean environment state. + """ + # Clear any existing environment variables that might interfere + env_vars_to_clear = [ + "DISCORD_TOKEN", + "API_KEY", + "DATABASE_URL", + "REDIS_URL", + ] + + for var in env_vars_to_clear: + monkeypatch.delenv(var, raising=False) + + +@pytest.fixture +def mock_async_context(): + """Provide a mock async context manager. + + Useful for testing async context managers without actual I/O. + """ + class MockAsyncContext: + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + pass + + return MockAsyncContext() + + +@pytest.fixture +def capture_logs(caplog): + """Capture and provide access to log messages during tests. + + Returns: + caplog: Pytest caplog fixture configured for the test + """ + caplog.set_level("DEBUG") + return caplog \ No newline at end of file diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_example_integration.py b/tests/integration/test_example_integration.py new file mode 100644 index 0000000..4db7b61 --- /dev/null +++ b/tests/integration/test_example_integration.py @@ -0,0 +1,45 @@ +"""Example integration test to demonstrate testing infrastructure.""" +import pytest +from unittest.mock import AsyncMock, patch + + +@pytest.mark.integration +class TestIntegrationExample: + """Example integration test class.""" + + def test_integration_with_mocked_external_service(self, mock_discord_client): + """Test simulating integration with Discord client.""" + # Use the mocked Discord client + assert mock_discord_client.user.name == "TestBot" + assert mock_discord_client.is_ready() is True + + # Simulate sending a message + mock_discord_client.send_message = AsyncMock(return_value={"id": "123", "content": "Test"}) + + @pytest.mark.asyncio + async def test_async_integration(self, mock_async_context): + """Test async integration scenario.""" + async with mock_async_context as ctx: + # Simulate async operations + assert ctx is not None + + def test_with_environment_variables(self, mock_env_vars): + """Test integration with environment variables.""" + import os + + # Verify environment variables are set + assert os.environ.get("DISCORD_TOKEN") == "test_discord_token" + assert os.environ.get("API_KEY") == "test_api_key" + assert os.environ.get("PORT") == "8000" + + @pytest.mark.slow + def test_slow_integration(self): + """Example of a slow test that might be skipped in quick runs.""" + import time + + # Simulate a slow operation + start = time.time() + time.sleep(0.1) # Simulate slow operation + duration = time.time() - start + + assert duration >= 0.1 \ No newline at end of file diff --git a/tests/test_setup_validation.py b/tests/test_setup_validation.py new file mode 100644 index 0000000..af8b7e6 --- /dev/null +++ b/tests/test_setup_validation.py @@ -0,0 +1,119 @@ +"""Validation tests to ensure the testing infrastructure is set up correctly.""" +import pytest +import sys +from pathlib import Path + +# Mark all async tests with pytest.mark.asyncio +pytestmark = pytest.mark.asyncio + + +def test_python_version(): + """Verify Python version is 3.8 or higher.""" + assert sys.version_info >= (3, 8), "Python 3.8 or higher is required" + + +def test_project_structure(): + """Verify the basic project structure exists.""" + project_root = Path(__file__).parent.parent + + # Check main directories + assert (project_root / "app").exists(), "app directory missing" + assert (project_root / "lib").exists(), "lib directory missing" + assert (project_root / "task").exists(), "task directory missing" + assert (project_root / "util").exists(), "util directory missing" + + # Check test directories + assert (project_root / "tests").exists(), "tests directory missing" + assert (project_root / "tests" / "unit").exists(), "unit tests directory missing" + assert (project_root / "tests" / "integration").exists(), "integration tests directory missing" + + +def test_conftest_fixtures(temp_dir, mock_config, mock_env_vars): + """Verify conftest fixtures are working properly.""" + # Test temp_dir fixture + assert temp_dir.exists() + assert temp_dir.is_dir() + + # Test mock_config fixture + assert isinstance(mock_config, dict) + assert "app" in mock_config + assert mock_config["app"]["name"] == "test_app" + + # Test mock_env_vars fixture + assert isinstance(mock_env_vars, dict) + assert "DISCORD_TOKEN" in mock_env_vars + assert mock_env_vars["DISCORD_TOKEN"] == "test_discord_token" + + +@pytest.mark.unit +def test_unit_marker(): + """Test that unit test marker is registered.""" + assert True, "Unit test marker is working" + + +@pytest.mark.integration +def test_integration_marker(): + """Test that integration test marker is registered.""" + assert True, "Integration test marker is working" + + +@pytest.mark.slow +def test_slow_marker(): + """Test that slow test marker is registered.""" + assert True, "Slow test marker is working" + + +def test_pytest_configuration(): + """Verify pytest is configured correctly.""" + import pytest + + # Check that pytest is installed + assert hasattr(pytest, "__version__") + + # Verify version is recent + major, minor = map(int, pytest.__version__.split(".")[:2]) + assert major >= 7, f"pytest version {pytest.__version__} is too old" + + +def test_coverage_configuration(): + """Verify coverage is configured correctly.""" + try: + import pytest_cov + assert hasattr(pytest_cov, "__version__") + except ImportError: + pytest.fail("pytest-cov is not installed") + + +def test_mock_configuration(): + """Verify pytest-mock is configured correctly.""" + try: + import pytest_mock + # pytest-mock doesn't expose __version__ directly + # Check for key functionality instead + assert hasattr(pytest_mock, "plugin") + assert hasattr(pytest_mock, "MockerFixture") + except ImportError: + pytest.fail("pytest-mock is not installed") + + +async def test_async_support(): + """Verify async test support is working.""" + import asyncio + + async def async_function(): + await asyncio.sleep(0.001) + return "async works" + + result = await async_function() + assert result == "async works" + + +def test_fixture_isolation(mock_env_vars): + """Verify fixtures provide proper test isolation.""" + import os + + # Verify test environment variable is set + assert os.environ.get("DISCORD_TOKEN") == "test_discord_token" + + # This should not affect other tests due to fixture isolation + os.environ["TEST_VAR"] = "test_value" \ No newline at end of file diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/test_example.py b/tests/unit/test_example.py new file mode 100644 index 0000000..3e1c45c --- /dev/null +++ b/tests/unit/test_example.py @@ -0,0 +1,74 @@ +"""Example unit test to demonstrate the testing infrastructure.""" +import pytest +from unittest.mock import Mock, patch + + +class TestExample: + """Example test class demonstrating various testing patterns.""" + + def test_simple_assertion(self): + """Basic test with simple assertion.""" + result = 2 + 2 + assert result == 4 + + def test_with_fixture(self, mock_config): + """Test using a fixture from conftest.py.""" + assert mock_config["app"]["name"] == "test_app" + assert mock_config["discord"]["token"] == "test_token" + + def test_with_mock(self, mocker): + """Test using pytest-mock for mocking.""" + # Create a mock function + mock_func = mocker.Mock(return_value="mocked value") + + # Call the mock + result = mock_func() + + # Verify + assert result == "mocked value" + mock_func.assert_called_once() + + @pytest.mark.unit + def test_with_marker(self): + """Test with custom marker.""" + assert True + + def test_exception_handling(self): + """Test exception handling.""" + with pytest.raises(ValueError, match="Invalid value"): + raise ValueError("Invalid value") + + def test_parametrized(self, temp_dir): + """Test using temp_dir fixture.""" + # Create a test file + test_file = temp_dir / "test.txt" + test_file.write_text("test content") + + # Verify + assert test_file.exists() + assert test_file.read_text() == "test content" + + +@pytest.mark.parametrize("input_val,expected", [ + (1, 2), + (2, 4), + (3, 6), + (4, 8), +]) +def test_parametrized_function(input_val, expected): + """Example of parametrized testing.""" + result = input_val * 2 + assert result == expected + + +@pytest.mark.asyncio +async def test_async_function(): + """Example async test.""" + import asyncio + + async def async_operation(): + await asyncio.sleep(0.01) + return "async result" + + result = await async_operation() + assert result == "async result" \ No newline at end of file