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
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Python API Server Tests

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
services:
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'

- name: Install uv
uses: astral-sh/setup-uv@v2
with:
cache: true

- name: Install dev dependencies with uv
working-directory: api-server
run: |
uv sync --group dev

- name: Run tests with pytest and coverage
working-directory: api-server
run: |
uv run pytest --cov=app --cov-report=xml --cov-report=term-missing -v --junitxml=pytest-report.xml

- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: exospherehost/exospherehost
files: api-server/coverage.xml
flags: unittests
name: codecov-coverage-report
fail_ci_if_error: true
7 changes: 5 additions & 2 deletions api-server/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ dependencies = [
"structlog>=25.4.0",
"uvicorn>=0.35.0",
"redis[hiredis]>=6.0.0"
]
]

[dependency-groups]
dev = [
"ruff>=0.12.2",
]
"pytest>=8.0.0",
"httpx>=0.27.0",
"pytest-cov>=5.0.0"
]
41 changes: 41 additions & 0 deletions api-server/tests/test_request_id_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import uuid
import pytest
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from app.middlewares.request_id_middleware import RequestIdMiddleware

# Minimal endpoint to test request ID logic
async def handler(request: Request):
return JSONResponse({"msg": "ok"})

@pytest.fixture
def client():
app = FastAPI()
app.add_middleware(RequestIdMiddleware) # Only the request ID middleware
app.add_api_route("/test", handler, methods=["GET"])
return TestClient(app)

def test_generates_uuid_when_missing(client):
resp = client.get("/test")
assert resp.status_code == 200
rid = resp.headers["x-exosphere-request-id"]
# Validate that a proper UUID was generated
parsed_uuid = uuid.UUID(rid)
assert str(parsed_uuid) == rid
assert resp.json() == {"msg": "ok"}

def test_preserves_valid_id(client):
valid_id = str(uuid.uuid4())
resp = client.get("/test", headers={"x-exosphere-request-id": valid_id})
assert resp.status_code == 200
assert resp.headers["x-exosphere-request-id"] == valid_id

def test_replaces_invalid_id(client):
resp = client.get("/test", headers={"x-exosphere-request-id": "bad-id"})
assert resp.status_code == 200
new_id = resp.headers["x-exosphere-request-id"]
assert new_id != "bad-id"
# Validate that a proper UUID was generated
parsed_uuid = uuid.UUID(new_id)
assert str(parsed_uuid) == new_id
54 changes: 54 additions & 0 deletions api-server/tests/test_unhandled_exceptions_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import pytest
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from app.middlewares.unhandled_exceptions_middleware import UnhandledExceptionsMiddleware

# Exception handlers to trigger different exceptions

async def fail(request: Request):
raise RuntimeError("boom")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async def fail_value_error(request: Request):
raise ValueError("Invalid value test")

async def fail_key_error(request: Request):
raise KeyError("Missing key test")

async def ok(request: Request):
return JSONResponse({"ok": True})

@pytest.fixture
def client():
app = FastAPI()
app.add_middleware(UnhandledExceptionsMiddleware)
app.add_api_route("/fail", fail, methods=["GET"])
app.add_api_route("/fail_value_error", fail_value_error, methods=["GET"])
app.add_api_route("/fail_key_error", fail_key_error, methods=["GET"])
app.add_api_route("/ok", ok, methods=["GET"])
return TestClient(app)

ERROR_RESPONSE = {
"success": False,
"detail": "internal server error, please reach out to support team at nivedit@exosphere.host"
}

def test_runtime_error_returns_expected_json(client):
resp = client.get("/fail")
assert resp.status_code == 500
assert resp.json() == ERROR_RESPONSE

def test_value_error_returns_expected_json(client):
resp = client.get("/fail_value_error")
assert resp.status_code == 500
assert resp.json() == ERROR_RESPONSE

def test_key_error_returns_expected_json(client):
resp = client.get("/fail_key_error")
assert resp.status_code == 500
assert resp.json() == ERROR_RESPONSE

def test_normal_request_passes_through(client):
resp = client.get("/ok")
assert resp.status_code == 200
assert resp.json() == {"ok": True}
Loading
Loading