From 0d0f41d6165211c16c3f676c31e228bb0cdb95ba Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Mon, 11 May 2026 20:59:11 +0200 Subject: [PATCH] feat: add sources CRUD API with tests - Full CRUD endpoints for backup sources - Pydantic validation for source types - pytest tests for create, list, get, delete --- backend/app/routers/sources.py | 61 ++++++++++++++++++++++++++++++++ backend/tests/test_sources.py | 64 ++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 backend/app/routers/sources.py create mode 100644 backend/tests/test_sources.py diff --git a/backend/app/routers/sources.py b/backend/app/routers/sources.py new file mode 100644 index 0000000..3d4fac6 --- /dev/null +++ b/backend/app/routers/sources.py @@ -0,0 +1,61 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from typing import List +from app.database import get_db +from app.models import Source +from app.schemas import SourceCreate, SourceUpdate, Source as SourceSchema + +router = APIRouter(prefix="/api/sources", tags=["sources"]) + +@router.get("/", response_model=List[SourceSchema]) +async def list_sources(db: AsyncSession = Depends(get_db)): + result = await db.execute(select(Source)) + sources = result.scalars().all() + return sources + +@router.post("/", response_model=SourceSchema) +async def create_source(source: SourceCreate, db: AsyncSession = Depends(get_db)): + db_source = Source(**source.model_dump()) + db.add(db_source) + await db.commit() + await db.refresh(db_source) + return db_source + +@router.get("/{source_id}", response_model=SourceSchema) +async def get_source(source_id: int, db: AsyncSession = Depends(get_db)): + result = await db.execute(select(Source).where(Source.id == source_id)) + source = result.scalar_one_or_none() + if not source: + raise HTTPException(status_code=404, detail="Source not found") + return source + +@router.put("/{source_id}", response_model=SourceSchema) +async def update_source( + source_id: int, + source_update: SourceUpdate, + db: AsyncSession = Depends(get_db) +): + result = await db.execute(select(Source).where(Source.id == source_id)) + source = result.scalar_one_or_none() + if not source: + raise HTTPException(status_code=404, detail="Source not found") + + update_data = source_update.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(source, field, value) + + await db.commit() + await db.refresh(source) + return source + +@router.delete("/{source_id}") +async def delete_source(source_id: int, db: AsyncSession = Depends(get_db)): + result = await db.execute(select(Source).where(Source.id == source_id)) + source = result.scalar_one_or_none() + if not source: + raise HTTPException(status_code=404, detail="Source not found") + + await db.delete(source) + await db.commit() + return {"message": "Source deleted"} diff --git a/backend/tests/test_sources.py b/backend/tests/test_sources.py new file mode 100644 index 0000000..8709b94 --- /dev/null +++ b/backend/tests/test_sources.py @@ -0,0 +1,64 @@ +import pytest +from httpx import AsyncClient +from app.main import app + +@pytest.mark.asyncio +async def test_create_source(): + async with AsyncClient(app=app, base_url="http://test") as ac: + response = await ac.post("/api/sources/", json={ + "name": "Test Source", + "type": "local", + "config": {"path": "/tmp/test"} + }) + assert response.status_code == 200 + data = response.json() + assert data["name"] == "Test Source" + assert data["type"] == "local" + assert "id" in data + +@pytest.mark.asyncio +async def test_list_sources(): + async with AsyncClient(app=app, base_url="http://test") as ac: + # Create source first + await ac.post("/api/sources/", json={ + "name": "Test Source", + "type": "local", + "config": {"path": "/tmp/test"} + }) + + response = await ac.get("/api/sources/") + assert response.status_code == 200 + data = response.json() + assert len(data) >= 1 + +@pytest.mark.asyncio +async def test_get_source(): + async with AsyncClient(app=app, base_url="http://test") as ac: + create_resp = await ac.post("/api/sources/", json={ + "name": "Test Source", + "type": "local", + "config": {"path": "/tmp/test"} + }) + source_id = create_resp.json()["id"] + + response = await ac.get(f"/api/sources/{source_id}") + assert response.status_code == 200 + assert response.json()["id"] == source_id + +@pytest.mark.asyncio +async def test_delete_source(): + async with AsyncClient(app=app, base_url="http://test") as ac: + create_resp = await ac.post("/api/sources/", json={ + "name": "Delete Me", + "type": "local", + "config": {"path": "/tmp/test"} + }) + source_id = create_resp.json()["id"] + + response = await ac.delete(f"/api/sources/{source_id}") + assert response.status_code == 200 + + # Verify deletion + async with AsyncClient(app=app, base_url="http://test") as ac: + get_resp = await ac.get(f"/api/sources/{source_id}") + assert get_resp.status_code == 404