feat: add FastAPI app factory and health check endpoint
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
57
app/main.py
Normal file
57
app/main.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
"""FastAPI application factory for SneakySwole.
|
||||||
|
|
||||||
|
Creates and configures the FastAPI app with routes, templates,
|
||||||
|
static files, and structured logging.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import structlog
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
|
from app.config import get_settings
|
||||||
|
from app.logging_config import setup_logging
|
||||||
|
from app.routes.health import router as health_router
|
||||||
|
|
||||||
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
|
# Template and static file directories
|
||||||
|
_BASE_DIR = Path(__file__).resolve().parent
|
||||||
|
TEMPLATES_DIR = _BASE_DIR / "templates"
|
||||||
|
STATIC_DIR = _BASE_DIR / "static"
|
||||||
|
|
||||||
|
|
||||||
|
def create_app() -> FastAPI:
|
||||||
|
"""Create and configure the FastAPI application.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A fully configured FastAPI application instance.
|
||||||
|
"""
|
||||||
|
settings = get_settings()
|
||||||
|
setup_logging(log_level=settings.app_log_level)
|
||||||
|
|
||||||
|
app = FastAPI(
|
||||||
|
title="SneakySwole",
|
||||||
|
description="Open-source workout tracking and programming",
|
||||||
|
version="0.1.0",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mount static files
|
||||||
|
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
||||||
|
|
||||||
|
# Jinja2 templates (available to routes via request.state or dependency)
|
||||||
|
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||||
|
app.state.templates = templates
|
||||||
|
|
||||||
|
# Register route modules
|
||||||
|
app.include_router(health_router)
|
||||||
|
|
||||||
|
logger.info("app_started", environment=settings.app_env)
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
# Uvicorn entry point
|
||||||
|
app = create_app()
|
||||||
23
app/routes/health.py
Normal file
23
app/routes/health.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
"""Health check route for monitoring and readiness probes.
|
||||||
|
|
||||||
|
Provides a simple GET /health endpoint that returns application
|
||||||
|
status, name, and version.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
router = APIRouter(tags=["health"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
async def health_check() -> dict:
|
||||||
|
"""Return application health status.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON with app name, version, and status.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"app": "sneakyswole",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"status": "ok",
|
||||||
|
}
|
||||||
28
tests/conftest.py
Normal file
28
tests/conftest.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""Shared test fixtures for the SneakySwole test suite."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _set_test_env() -> None:
|
||||||
|
"""Ensure required env vars are set for all tests."""
|
||||||
|
with patch.dict(os.environ, {
|
||||||
|
"ADMIN_USERNAME": "testadmin",
|
||||||
|
"ADMIN_PASSWORD": "testpass123",
|
||||||
|
"APP_ENV": "development",
|
||||||
|
"DATABASE_URL": "sqlite:///data/test_sneakyswole.db",
|
||||||
|
}, clear=False):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client() -> TestClient:
|
||||||
|
"""Create a FastAPI TestClient for integration tests."""
|
||||||
|
from app.main import create_app
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
return TestClient(app)
|
||||||
12
tests/test_app.py
Normal file
12
tests/test_app.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
"""Tests for the FastAPI application factory."""
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
class TestCreateApp:
|
||||||
|
"""Tests for the application factory and basic setup."""
|
||||||
|
|
||||||
|
def test_app_starts_without_error(self, client: TestClient) -> None:
|
||||||
|
"""The app should initialize and serve requests."""
|
||||||
|
response = client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
@@ -11,10 +11,14 @@ class TestSettings:
|
|||||||
|
|
||||||
def test_settings_loads_defaults(self) -> None:
|
def test_settings_loads_defaults(self) -> None:
|
||||||
"""Settings should have sensible defaults for all fields."""
|
"""Settings should have sensible defaults for all fields."""
|
||||||
with patch.dict(os.environ, {
|
env = {
|
||||||
"ADMIN_USERNAME": "testadmin",
|
"ADMIN_USERNAME": "testadmin",
|
||||||
"ADMIN_PASSWORD": "testpass123",
|
"ADMIN_PASSWORD": "testpass123",
|
||||||
}, clear=False):
|
}
|
||||||
|
# Remove DATABASE_URL so we test the actual default
|
||||||
|
env_clean = {k: v for k, v in os.environ.items() if k != "DATABASE_URL"}
|
||||||
|
env_clean.update(env)
|
||||||
|
with patch.dict(os.environ, env_clean, clear=True):
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
assert settings.admin_username == "testadmin"
|
assert settings.admin_username == "testadmin"
|
||||||
assert settings.admin_password == "testpass123"
|
assert settings.admin_password == "testpass123"
|
||||||
|
|||||||
30
tests/test_health.py
Normal file
30
tests/test_health.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
"""Tests for the health check endpoint."""
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
class TestHealthCheck:
|
||||||
|
"""Tests for GET /health."""
|
||||||
|
|
||||||
|
def test_health_returns_200(self, client: TestClient) -> None:
|
||||||
|
"""GET /health should return HTTP 200."""
|
||||||
|
response = client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
def test_health_returns_status_ok(self, client: TestClient) -> None:
|
||||||
|
"""GET /health should return a JSON body with status 'ok'."""
|
||||||
|
response = client.get("/health")
|
||||||
|
data = response.json()
|
||||||
|
assert data["status"] == "ok"
|
||||||
|
|
||||||
|
def test_health_includes_app_name(self, client: TestClient) -> None:
|
||||||
|
"""GET /health should include the application name."""
|
||||||
|
response = client.get("/health")
|
||||||
|
data = response.json()
|
||||||
|
assert data["app"] == "sneakyswole"
|
||||||
|
|
||||||
|
def test_health_includes_version(self, client: TestClient) -> None:
|
||||||
|
"""GET /health should include the application version."""
|
||||||
|
response = client.get("/health")
|
||||||
|
data = response.json()
|
||||||
|
assert "version" in data
|
||||||
Reference in New Issue
Block a user