feat: add FastAPI app factory and health check endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 09:12:31 -06:00
parent fb4b948681
commit b3b34222c8
6 changed files with 156 additions and 2 deletions

57
app/main.py Normal file
View 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
View 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
View 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
View 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

View File

@@ -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
View 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