Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ COMPASS_MODEL=ollama/gemma4:e4b
COMPASS_DOCS_PATH=./data/docs
COMPASS_WORKSPACE=./data/index
COMPASS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173
COMPASS_API_KEY= # leave empty to disable auth (local dev)
12 changes: 11 additions & 1 deletion backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
from contextlib import asynccontextmanager

from dotenv import load_dotenv
from fastapi import FastAPI, Request, UploadFile, File, HTTPException
from fastapi import FastAPI, Request, UploadFile, File, HTTPException, Security
from fastapi.security.api_key import APIKeyHeader
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

Expand All @@ -24,6 +25,14 @@
COMPASS_MODEL = os.getenv("COMPASS_MODEL", "ollama/gemma4:e4b")
DOCS_PATH = Path(os.getenv("COMPASS_DOCS_PATH", "./data/docs"))
WORKSPACE = Path(os.getenv("COMPASS_WORKSPACE", "./data/index"))
API_KEY = os.getenv("COMPASS_API_KEY") # None = auth disabled

_api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)


async def verify_api_key(key: str = Security(_api_key_header)):
if API_KEY and key != API_KEY:
raise HTTPException(status_code=401, detail="Invalid or missing API key.")

os.environ["OLLAMA_API_BASE"] = OLLAMA_API_BASE

Expand Down Expand Up @@ -57,6 +66,7 @@ async def lifespan(app: FastAPI):
description="El cerebro operativo de tu empresa.",
version="0.2.0",
lifespan=lifespan,
dependencies=[Security(verify_api_key)],
)

ALLOWED_ORIGINS = [
Expand Down
46 changes: 46 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,52 @@
No Ollama, no disk I/O, no SQLite required — all external calls are mocked.
"""

import pytest
from unittest.mock import patch


# ---------------------------------------------------------------------------
# Authentication
# ---------------------------------------------------------------------------

class TestAuth:
def test_no_key_required_when_env_unset(self, client_empty):
"""Auth is disabled by default — all requests pass through."""
assert client_empty.get("/health").status_code == 200

def test_valid_key_is_accepted(self, client_empty):
with patch("backend.main.API_KEY", "secret"):
r = client_empty.get("/health", headers={"X-API-Key": "secret"})
assert r.status_code == 200

def test_invalid_key_returns_401(self, client_empty):
with patch("backend.main.API_KEY", "secret"):
r = client_empty.get("/health", headers={"X-API-Key": "wrong"})
assert r.status_code == 401

def test_missing_key_returns_401(self, client_empty):
with patch("backend.main.API_KEY", "secret"):
r = client_empty.get("/health")
assert r.status_code == 401

def test_auth_applies_to_chat(self, client_with_docs):
with patch("backend.main.API_KEY", "secret"):
r = client_with_docs.post("/chat", json={"question": "Hi"})
assert r.status_code == 401

def test_auth_applies_to_upload(self, client_empty):
with patch("backend.main.API_KEY", "secret"):
r = client_empty.post(
"/upload",
files={"file": ("doc.md", b"# Test", "text/markdown")},
)
assert r.status_code == 401

def test_auth_applies_to_documents(self, client_empty):
with patch("backend.main.API_KEY", "secret"):
r = client_empty.get("/documents")
assert r.status_code == 401


# ---------------------------------------------------------------------------
# GET /health
Expand Down
Loading