diff --git a/src/backend/api/dependencies.py b/src/backend/api/dependencies.py new file mode 100644 index 0000000..f412284 --- /dev/null +++ b/src/backend/api/dependencies.py @@ -0,0 +1,8 @@ +from fastapi import Header, HTTPException + +from src.backend.config import settings + + +async def verify_api_key(x_api_key: str | None = Header(default=None)): + if x_api_key is None or x_api_key != settings.api_key: + raise HTTPException(status_code=403, detail="API key non valida") diff --git a/src/backend/api/routes/health.py b/src/backend/api/routes/health.py new file mode 100644 index 0000000..57b127b --- /dev/null +++ b/src/backend/api/routes/health.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter, Depends + +from src.backend.api.dependencies import verify_api_key +from src.backend.models.api import ok + +router = APIRouter(dependencies=[Depends(verify_api_key)]) + + +@router.get("/health", summary="Health check", tags=["system"]) +async def health(): + return ok({"status": "running"}) diff --git a/src/backend/main.py b/src/backend/main.py new file mode 100644 index 0000000..8fc28af --- /dev/null +++ b/src/backend/main.py @@ -0,0 +1,17 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI + +from src.backend.api.routes.health import router as health_router +from src.backend.db.database import init_db + + +@asynccontextmanager +async def lifespan(app: FastAPI): + await init_db() + yield + + +app = FastAPI(title="OpusAgent", version="0.1.0", lifespan=lifespan) + +app.include_router(health_router, prefix="/api") diff --git a/tests/test_api_topics.py b/tests/test_api_topics.py new file mode 100644 index 0000000..2d4a87e --- /dev/null +++ b/tests/test_api_topics.py @@ -0,0 +1,150 @@ +import pytest +from httpx import AsyncClient, ASGITransport + + +@pytest.fixture +def app(): + from src.backend.main import app + return app + + +@pytest.fixture +async def client(app): + transport = ASGITransport(app=app) + async with AsyncClient( + transport=transport, + base_url="http://test", + headers={"X-Api-Key": "test-key-12345"}, + ) as ac: + yield ac + + +@pytest.fixture +async def client_no_auth(app): + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as ac: + yield ac + + +@pytest.mark.asyncio +async def test_request_without_api_key_returns_403(client_no_auth): + resp = await client_no_auth.get("/api/health") + assert resp.status_code == 403 + + +@pytest.mark.asyncio +async def test_request_with_wrong_api_key_returns_403(app): + transport = ASGITransport(app=app) + async with AsyncClient( + transport=transport, + base_url="http://test", + headers={"X-Api-Key": "wrong-key"}, + ) as ac: + resp = await ac.get("/api/health") + assert resp.status_code == 403 + + +@pytest.mark.asyncio +async def test_create_topic(client): + resp = await client.post( + "/api/topics", + json={ + "name": "coding", + "system_prompt": "You are an expert coder.", + "summary": "Coding topic", + "model": "claude-sonnet-4-6", + }, + ) + assert resp.status_code == 201 + data = resp.json() + assert data["success"] is True + assert data["data"]["name"] == "coding" + assert data["data"]["model"] == "claude-sonnet-4-6" + assert "id" in data["data"] + + +@pytest.mark.asyncio +async def test_create_topic_minimal(client): + resp = await client.post( + "/api/topics", + json={"name": "analysis", "system_prompt": "You analyze data."}, + ) + assert resp.status_code == 201 + data = resp.json() + assert data["data"]["summary"] == "" + assert data["data"]["model"] is None + + +@pytest.mark.asyncio +async def test_create_topic_duplicate_name_fails(client): + await client.post( + "/api/topics", + json={"name": "coding", "system_prompt": "Prompt 1"}, + ) + resp = await client.post( + "/api/topics", + json={"name": "coding", "system_prompt": "Prompt 2"}, + ) + assert resp.status_code == 409 + + +@pytest.mark.asyncio +async def test_list_topics(client): + await client.post( + "/api/topics", json={"name": "a", "system_prompt": "A"} + ) + await client.post( + "/api/topics", json={"name": "b", "system_prompt": "B"} + ) + resp = await client.get("/api/topics") + assert resp.status_code == 200 + data = resp.json() + assert len(data["data"]) == 2 + + +@pytest.mark.asyncio +async def test_get_topic(client): + create_resp = await client.post( + "/api/topics", json={"name": "coding", "system_prompt": "Code"} + ) + topic_id = create_resp.json()["data"]["id"] + + resp = await client.get(f"/api/topics/{topic_id}") + assert resp.status_code == 200 + assert resp.json()["data"]["name"] == "coding" + + +@pytest.mark.asyncio +async def test_get_topic_not_found(client): + resp = await client.get("/api/topics/nonexistent-id") + assert resp.status_code == 404 + + +@pytest.mark.asyncio +async def test_update_topic(client): + create_resp = await client.post( + "/api/topics", json={"name": "coding", "system_prompt": "Old"} + ) + topic_id = create_resp.json()["data"]["id"] + + resp = await client.put( + f"/api/topics/{topic_id}", + json={"system_prompt": "New prompt"}, + ) + assert resp.status_code == 200 + assert resp.json()["data"]["system_prompt"] == "New prompt" + assert resp.json()["data"]["name"] == "coding" + + +@pytest.mark.asyncio +async def test_delete_topic(client): + create_resp = await client.post( + "/api/topics", json={"name": "coding", "system_prompt": "Code"} + ) + topic_id = create_resp.json()["data"]["id"] + + resp = await client.delete(f"/api/topics/{topic_id}") + assert resp.status_code == 200 + + resp = await client.get(f"/api/topics/{topic_id}") + assert resp.status_code == 404