From 8b75fb438fbb86972a83c6cf8d5d82262e85741d Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 25 May 2026 18:41:57 +0200 Subject: [PATCH] feat: add Pydantic Settings configuration --- pyproject.toml | 9 ++++ src/backend/config.py | 25 ++++++++++ tests/conftest.py | 30 +++++++++++ tests/test_services.py | 111 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+) create mode 100644 src/backend/config.py create mode 100644 tests/conftest.py create mode 100644 tests/test_services.py diff --git a/pyproject.toml b/pyproject.toml index 7ddbcc3..2a88624 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,3 +23,12 @@ dev = [ [project.scripts] start = "uvicorn src.backend.main:app --host 0.0.0.0 --port 8000" dev = "uvicorn src.backend.main:app --reload --host 0.0.0.0 --port 8000" + +[dependency-groups] +dev = [ + "pytest>=9.0.3", + "pytest-asyncio>=1.3.0", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" diff --git a/src/backend/config.py b/src/backend/config.py new file mode 100644 index 0000000..6d6e679 --- /dev/null +++ b/src/backend/config.py @@ -0,0 +1,25 @@ +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + api_host: str = "0.0.0.0" + api_port: int = 8000 + + api_key: str + + rate_limit_per_minute: int = 10 + rate_limit_per_hour: int = 100 + + worker_url: str = "http://claude-worker:3001" + + db_path: str = "/data/opus-agent.db" + + claude_model: str = "claude-sonnet-4-6" + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + } + + +settings = Settings() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ba6fe82 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,30 @@ +import os +import tempfile + +_test_dir = tempfile.mkdtemp() + +os.environ.setdefault("API_KEY", "test-key-12345") +os.environ.setdefault("DB_PATH", os.path.join(_test_dir, "test.db")) +os.environ.setdefault("WORKER_URL", "http://localhost:3001") +os.environ.setdefault("CLAUDE_MODEL", "claude-sonnet-4-6") +os.environ.setdefault("RATE_LIMIT_PER_MINUTE", "100") +os.environ.setdefault("RATE_LIMIT_PER_HOUR", "1000") +os.environ.setdefault("API_HOST", "0.0.0.0") +os.environ.setdefault("API_PORT", "8000") + +import pytest + + +@pytest.fixture(autouse=True) +async def setup_db(): + from src.backend.db.database import init_db, get_db + + await init_db() + yield + db = await get_db() + try: + await db.execute("DELETE FROM requests") + await db.execute("DELETE FROM topics") + await db.commit() + finally: + await db.close() diff --git a/tests/test_services.py b/tests/test_services.py new file mode 100644 index 0000000..e50ea21 --- /dev/null +++ b/tests/test_services.py @@ -0,0 +1,111 @@ +from src.backend.config import settings + +import pytest + + +def test_settings_loads_api_key(): + assert settings.api_key == "test-key-12345" + + +def test_settings_loads_db_path(): + assert "test.db" in settings.db_path + + +def test_settings_loads_worker_url(): + assert settings.worker_url == "http://localhost:3001" + + +def test_settings_loads_claude_model(): + assert settings.claude_model == "claude-sonnet-4-6" + + +def test_settings_loads_rate_limits(): + assert settings.rate_limit_per_minute == 100 + assert settings.rate_limit_per_hour == 1000 + + +@pytest.mark.asyncio +async def test_init_db_creates_tables(): + from src.backend.db.database import init_db, get_db + + await init_db() + db = await get_db() + try: + cursor = await db.execute( + "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" + ) + tables = [row[0] for row in await cursor.fetchall()] + assert "topics" in tables + assert "requests" in tables + finally: + await db.close() + + +@pytest.mark.asyncio +async def test_foreign_keys_enabled(): + from src.backend.db.database import init_db, get_db + + await init_db() + db = await get_db() + try: + cursor = await db.execute("PRAGMA foreign_keys") + row = await cursor.fetchone() + assert row[0] == 1 + finally: + await db.close() + + +def test_topic_create_model_validates(): + from src.backend.models.api import TopicCreate + + topic = TopicCreate( + name="coding", + system_prompt="You are an expert coder.", + summary="Coding questions", + model="claude-sonnet-4-6", + ) + assert topic.name == "coding" + assert topic.model == "claude-sonnet-4-6" + + +def test_topic_create_model_optional_fields(): + from src.backend.models.api import TopicCreate + + topic = TopicCreate(name="coding", system_prompt="You are an expert coder.") + assert topic.summary == "" + assert topic.model is None + + +def test_request_create_model_validates(): + from src.backend.models.api import RequestCreate + + req = RequestCreate( + prompt="How to do X?", + topic_id="550e8400-e29b-41d4-a716-446655440000", + ) + assert req.session_id is None + assert req.model is None + assert req.summarize is False + + +def test_model_validation_rejects_invalid(): + from src.backend.models.api import TopicCreate + from pydantic import ValidationError + + with pytest.raises(ValidationError): + TopicCreate(name="test", system_prompt="test", model="gpt-4") + + +def test_envelope_ok(): + from src.backend.models.api import ok + + result = ok({"key": "value"}) + assert result == {"success": True, "data": {"key": "value"}, "error": None} + + +def test_envelope_fail(): + from src.backend.models.api import fail + + result = fail("NOT_FOUND", "Resource not found") + assert result["success"] is False + assert result["error"]["code"] == "NOT_FOUND"