diff --git a/server/app.py b/server/app.py new file mode 100644 index 0000000..6cded95 --- /dev/null +++ b/server/app.py @@ -0,0 +1,37 @@ +"""MusicFetch REST API. Plain HTTP behind an upstream TLS reverse proxy.""" +import os + +from fastapi import Depends, FastAPI, Header, HTTPException +from fastapi.responses import JSONResponse + +from . import actions, jobs, mf + +API_KEY = os.environ.get("MUSICFETCH_API_KEY", "") +ROOT = os.environ.get("MUSICFETCH_ROOT", "/media/music") + +app = FastAPI(title="MusicFetch API") + + +def require_key(x_api_key: str = Header(default="")): + if not API_KEY or x_api_key != API_KEY: + raise HTTPException(status_code=401, detail="Invalid API key.") + + +@app.exception_handler(HTTPException) +async def _http_exc(_req, exc: HTTPException): + # Always return a Siri-speakable {"message": ...} body. + return JSONResponse(status_code=exc.status_code, content={"message": exc.detail}) + + +@app.get("/health") +def health(): + return {"status": "ok"} + + +# Minimal stub so that auth tests can verify 401 behaviour before the real +# /fetch implementation lands in Task 5. This route MUST carry require_key as +# a dependency so unauthenticated / wrong-key requests are rejected here. +@app.post("/fetch", dependencies=[Depends(require_key)]) +def fetch_stub(q: str = ""): + # Task 5 will replace this body with the full search-and-download logic. + raise HTTPException(status_code=501, detail="Not yet implemented.") diff --git a/server/requirements.txt b/server/requirements.txt new file mode 100644 index 0000000..4a767ef --- /dev/null +++ b/server/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn[standard] +requests +ytmusicapi +rich diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..9255b86 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,16 @@ +import os +import pytest + +os.environ.setdefault("MUSICFETCH_API_KEY", "test-key") + + +@pytest.fixture +def client(): + from fastapi.testclient import TestClient + from server.app import app + return TestClient(app) + + +@pytest.fixture +def auth(): + return {"X-API-Key": "test-key"} diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 0000000..5339bbd --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,16 @@ +def test_health_no_auth(client): + r = client.get("/health") + assert r.status_code == 200 + assert r.json() == {"status": "ok"} + + +def test_fetch_requires_key(client): + r = client.post("/fetch", params={"q": "anything"}) + assert r.status_code == 401 + assert "message" in r.json() + + +def test_fetch_rejects_wrong_key(client): + r = client.post("/fetch", params={"q": "anything"}, + headers={"X-API-Key": "wrong"}) + assert r.status_code == 401