diff --git a/infrastructure.md b/infrastructure.md index 3b0a3e0..12fba67 100644 --- a/infrastructure.md +++ b/infrastructure.md @@ -122,6 +122,16 @@ conda activate AniWorld - `PUT /api/config` - Update configuration - `POST /api/setup` - Initial setup +### Configuration API Notes + +- The configuration endpoints are exposed under `/api/config` and + operate primarily on a JSON-serializable `AppConfig` model. They are + designed to be lightweight and avoid performing IO during validation + (the `/api/config/validate` endpoint runs in-memory checks only). +- Persistence of configuration changes is intentionally "best-effort" + for now and mirrors fields into the runtime settings object. A + follow-up task should add durable storage (file or DB) for configs. + ### Anime Management - `GET /api/anime` - List anime with missing episodes diff --git a/src/server/api/config.py b/src/server/api/config.py new file mode 100644 index 0000000..ad8c385 --- /dev/null +++ b/src/server/api/config.py @@ -0,0 +1,68 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, HTTPException, status + +from src.config.settings import settings +from src.server.models.config import AppConfig, ConfigUpdate, ValidationResult +from src.server.utils.dependencies import require_auth + +router = APIRouter(prefix="/api/config", tags=["config"]) + + +@router.get("", response_model=AppConfig) +def get_config(auth: Optional[dict] = Depends(require_auth)) -> AppConfig: + """Return current application configuration (read-only).""" + # Construct AppConfig from pydantic-settings where possible + cfg_data = { + "name": getattr(settings, "app_name", "Aniworld"), + "data_dir": getattr(settings, "data_dir", "data"), + "scheduler": getattr(settings, "scheduler", {}), + "logging": getattr(settings, "logging", {}), + "backup": getattr(settings, "backup", {}), + "other": getattr(settings, "other", {}), + } + try: + return AppConfig(**cfg_data) + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to read config: {e}") + + +@router.put("", response_model=AppConfig) +def update_config(update: ConfigUpdate, auth: dict = Depends(require_auth)) -> AppConfig: + """Apply an update to the configuration and return the new config. + + Note: persistence strategy for settings is out-of-scope for this task. + This endpoint updates the in-memory Settings where possible and returns + the merged result as an AppConfig. + """ + # Build current AppConfig from settings then apply update + current = get_config(auth) + new_cfg = update.apply_to(current) + + # Mirror some fields back into pydantic-settings 'settings' where safe. + # Avoid writing secrets or unsupported fields. + try: + if new_cfg.data_dir: + setattr(settings, "data_dir", new_cfg.data_dir) + # scheduler/logging/backup/other kept in memory only for now + setattr(settings, "scheduler", new_cfg.scheduler.model_dump()) + setattr(settings, "logging", new_cfg.logging.model_dump()) + setattr(settings, "backup", new_cfg.backup.model_dump()) + setattr(settings, "other", new_cfg.other) + except Exception: + # Best-effort; do not fail the request if persistence is not available + pass + + return new_cfg + + +@router.post("/validate", response_model=ValidationResult) +def validate_config(cfg: AppConfig, auth: dict = Depends(require_auth)) -> ValidationResult: + """Validate a provided AppConfig without applying it. + + Returns ValidationResult with any validation errors. + """ + try: + return cfg.validate() + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) diff --git a/tests/api/test_config_endpoints.py b/tests/api/test_config_endpoints.py new file mode 100644 index 0000000..da0668a --- /dev/null +++ b/tests/api/test_config_endpoints.py @@ -0,0 +1,36 @@ +from fastapi.testclient import TestClient + +from src.server.fastapi_app import app +from src.server.models.config import AppConfig, SchedulerConfig + +client = TestClient(app) + + +def test_get_config_public(): + resp = client.get("/api/config") + assert resp.status_code == 200 + data = resp.json() + assert "name" in data + assert "data_dir" in data + + +def test_validate_config(): + cfg = { + "name": "Aniworld", + "data_dir": "data", + "scheduler": {"enabled": True, "interval_minutes": 30}, + "logging": {"level": "INFO"}, + "backup": {"enabled": False}, + "other": {}, + } + resp = client.post("/api/config/validate", json=cfg) + assert resp.status_code == 200 + body = resp.json() + assert body.get("valid") is True + + +def test_update_config_unauthorized(): + # update requires auth; without auth should be 401 + update = {"scheduler": {"enabled": False}} + resp = client.put("/api/config", json=update) + assert resp.status_code in (401, 422)