Skip to content

Commit 267ab06

Browse files
Unit tests middleware for API Server and CI/CD for the same (#130)
* Add separated middleware tests and CI workflow * Update middleware tests, dependencies, and CI workflow * Update .github/workflows/ci.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update .github/workflows/ci.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update api-server/pyproject.toml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update api-server/pyproject.toml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update pyproject.toml * Update .github/workflows/ci.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update pyproject.toml * Update .github/workflows/ci.yml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update api-server/tests/test_request_id_middleware.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update api-server/tests/test_request_id_middleware.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update api-server/tests/test_unhandled_exceptions_middleware.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Address reviewer feedback and CI errors: Python version, error response test, Redis CI service and artefacts * Enable coverage reporting via Codecov and cleanup nits * Enable coverage reporting via Codecov and cleanup nits remaining changes * Fix YAML structure/codecov step; cleanup pyproject formatting --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent bedae6d commit 267ab06

5 files changed

Lines changed: 332 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: Python API Server Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
services:
13+
redis:
14+
image: redis:7
15+
ports:
16+
- 6379:6379
17+
options: >-
18+
--health-cmd "redis-cli ping"
19+
--health-interval 10s
20+
--health-timeout 5s
21+
--health-retries 5
22+
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v4
26+
27+
- name: Set up Python
28+
uses: actions/setup-python@v5
29+
with:
30+
python-version: '3.12'
31+
32+
- name: Install uv
33+
uses: astral-sh/setup-uv@v2
34+
with:
35+
cache: true
36+
37+
- name: Install dev dependencies with uv
38+
working-directory: api-server
39+
run: |
40+
uv sync --group dev
41+
42+
- name: Run tests with pytest and coverage
43+
working-directory: api-server
44+
run: |
45+
uv run pytest --cov=app --cov-report=xml --cov-report=term-missing -v --junitxml=pytest-report.xml
46+
47+
- name: Upload coverage reports to Codecov
48+
uses: codecov/codecov-action@v5
49+
with:
50+
token: ${{ secrets.CODECOV_TOKEN }}
51+
slug: exospherehost/exospherehost
52+
files: api-server/coverage.xml
53+
flags: unittests
54+
name: codecov-coverage-report
55+
fail_ci_if_error: true

api-server/pyproject.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ dependencies = [
1616
"structlog>=25.4.0",
1717
"uvicorn>=0.35.0",
1818
"redis[hiredis]>=6.0.0"
19-
]
19+
]
2020

2121
[dependency-groups]
2222
dev = [
2323
"ruff>=0.12.2",
24-
]
24+
"pytest>=8.0.0",
25+
"httpx>=0.27.0",
26+
"pytest-cov>=5.0.0"
27+
]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import uuid
2+
import pytest
3+
from fastapi import FastAPI, Request
4+
from fastapi.responses import JSONResponse
5+
from fastapi.testclient import TestClient
6+
from app.middlewares.request_id_middleware import RequestIdMiddleware
7+
8+
# Minimal endpoint to test request ID logic
9+
async def handler(request: Request):
10+
return JSONResponse({"msg": "ok"})
11+
12+
@pytest.fixture
13+
def client():
14+
app = FastAPI()
15+
app.add_middleware(RequestIdMiddleware) # Only the request ID middleware
16+
app.add_api_route("/test", handler, methods=["GET"])
17+
return TestClient(app)
18+
19+
def test_generates_uuid_when_missing(client):
20+
resp = client.get("/test")
21+
assert resp.status_code == 200
22+
rid = resp.headers["x-exosphere-request-id"]
23+
# Validate that a proper UUID was generated
24+
parsed_uuid = uuid.UUID(rid)
25+
assert str(parsed_uuid) == rid
26+
assert resp.json() == {"msg": "ok"}
27+
28+
def test_preserves_valid_id(client):
29+
valid_id = str(uuid.uuid4())
30+
resp = client.get("/test", headers={"x-exosphere-request-id": valid_id})
31+
assert resp.status_code == 200
32+
assert resp.headers["x-exosphere-request-id"] == valid_id
33+
34+
def test_replaces_invalid_id(client):
35+
resp = client.get("/test", headers={"x-exosphere-request-id": "bad-id"})
36+
assert resp.status_code == 200
37+
new_id = resp.headers["x-exosphere-request-id"]
38+
assert new_id != "bad-id"
39+
# Validate that a proper UUID was generated
40+
parsed_uuid = uuid.UUID(new_id)
41+
assert str(parsed_uuid) == new_id
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import pytest
2+
from fastapi import FastAPI, Request
3+
from fastapi.responses import JSONResponse
4+
from fastapi.testclient import TestClient
5+
from app.middlewares.unhandled_exceptions_middleware import UnhandledExceptionsMiddleware
6+
7+
# Exception handlers to trigger different exceptions
8+
9+
async def fail(request: Request):
10+
raise RuntimeError("boom")
11+
12+
async def fail_value_error(request: Request):
13+
raise ValueError("Invalid value test")
14+
15+
async def fail_key_error(request: Request):
16+
raise KeyError("Missing key test")
17+
18+
async def ok(request: Request):
19+
return JSONResponse({"ok": True})
20+
21+
@pytest.fixture
22+
def client():
23+
app = FastAPI()
24+
app.add_middleware(UnhandledExceptionsMiddleware)
25+
app.add_api_route("/fail", fail, methods=["GET"])
26+
app.add_api_route("/fail_value_error", fail_value_error, methods=["GET"])
27+
app.add_api_route("/fail_key_error", fail_key_error, methods=["GET"])
28+
app.add_api_route("/ok", ok, methods=["GET"])
29+
return TestClient(app)
30+
31+
ERROR_RESPONSE = {
32+
"success": False,
33+
"detail": "internal server error, please reach out to support team at nivedit@exosphere.host"
34+
}
35+
36+
def test_runtime_error_returns_expected_json(client):
37+
resp = client.get("/fail")
38+
assert resp.status_code == 500
39+
assert resp.json() == ERROR_RESPONSE
40+
41+
def test_value_error_returns_expected_json(client):
42+
resp = client.get("/fail_value_error")
43+
assert resp.status_code == 500
44+
assert resp.json() == ERROR_RESPONSE
45+
46+
def test_key_error_returns_expected_json(client):
47+
resp = client.get("/fail_key_error")
48+
assert resp.status_code == 500
49+
assert resp.json() == ERROR_RESPONSE
50+
51+
def test_normal_request_passes_through(client):
52+
resp = client.get("/ok")
53+
assert resp.status_code == 200
54+
assert resp.json() == {"ok": True}

0 commit comments

Comments
 (0)