feat(01-01): add test scaffold, health and static asset tests, fix deprecated APIs
- requirements-dev.txt with pytest and httpx
- tests/conftest.py: client and tmp_data_dir fixtures
- tests/test_health.py: GET /health returns 200 with {"status": "ok"}
- tests/test_static.py: no CDN URLs in templates, dashboard returns 200
- Fix imptune/api/pages.py: use request= kwarg in TemplateResponse (Starlette compat)
- Fix imptune/main.py: replace deprecated on_event with asynccontextmanager lifespan
This commit is contained in:
@@ -11,9 +11,9 @@ templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templa
|
|||||||
@router.get("/", response_class=HTMLResponse)
|
@router.get("/", response_class=HTMLResponse)
|
||||||
def dashboard(request: Request):
|
def dashboard(request: Request):
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"dashboard.html",
|
request=request,
|
||||||
{
|
name="dashboard.html",
|
||||||
"request": request,
|
context={
|
||||||
"recent_printers": [],
|
"recent_printers": [],
|
||||||
"recent_packages": [],
|
"recent_packages": [],
|
||||||
},
|
},
|
||||||
|
|||||||
+11
-9
@@ -1,3 +1,5 @@
|
|||||||
|
import os
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
@@ -6,7 +8,15 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
from imptune.api import health, pages
|
from imptune.api import health, pages
|
||||||
from imptune.config import DATA_DIR, DRIVERS_DIR
|
from imptune.config import DATA_DIR, DRIVERS_DIR
|
||||||
|
|
||||||
app = FastAPI(title="ImpTune")
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: FastAPI):
|
||||||
|
os.makedirs(DATA_DIR, exist_ok=True)
|
||||||
|
os.makedirs(DRIVERS_DIR, exist_ok=True)
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
app = FastAPI(title="ImpTune", lifespan=lifespan)
|
||||||
|
|
||||||
# Serve baked-in static assets (pico.min.css, htmx.min.js, alpine.min.js)
|
# Serve baked-in static assets (pico.min.css, htmx.min.js, alpine.min.js)
|
||||||
_static_dir = Path(__file__).parent / "static"
|
_static_dir = Path(__file__).parent / "static"
|
||||||
@@ -15,11 +25,3 @@ app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static")
|
|||||||
# Register routers
|
# Register routers
|
||||||
app.include_router(health.router)
|
app.include_router(health.router)
|
||||||
app.include_router(pages.router)
|
app.include_router(pages.router)
|
||||||
|
|
||||||
|
|
||||||
@app.on_event("startup")
|
|
||||||
def on_startup():
|
|
||||||
import os
|
|
||||||
|
|
||||||
os.makedirs(DATA_DIR, exist_ok=True)
|
|
||||||
os.makedirs(DRIVERS_DIR, exist_ok=True)
|
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pytest>=8.0
|
||||||
|
httpx>=0.27
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def client(tmp_data_dir):
|
||||||
|
from imptune.main import app
|
||||||
|
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tmp_data_dir(tmp_path, monkeypatch):
|
||||||
|
"""Set DATA_DIR to a temp directory so tests don't write to /data."""
|
||||||
|
data_dir = tmp_path / "data"
|
||||||
|
data_dir.mkdir()
|
||||||
|
monkeypatch.setenv("DATA_DIR", str(data_dir))
|
||||||
|
# Patch config module so the app uses the temp dir
|
||||||
|
import imptune.config as cfg
|
||||||
|
|
||||||
|
cfg.DATA_DIR = str(data_dir)
|
||||||
|
cfg.DB_PATH = str(data_dir / "imptune.db")
|
||||||
|
cfg.DRIVERS_DIR = str(data_dir / "drivers")
|
||||||
|
return data_dir
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
def test_health_returns_200(client):
|
||||||
|
"""GET /health returns 200 with {"status": "ok"}."""
|
||||||
|
response = client.get("/health")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json() == {"status": "ok"}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
CDN_PATTERNS = re.compile(
|
||||||
|
r"(cdn\.jsdelivr\.net|unpkg\.com|cdnjs\.com)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Matches href="https://..." or src="https://..."
|
||||||
|
EXTERNAL_URL_PATTERN = re.compile(
|
||||||
|
r'(?:href|src)=["\']https?://',
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_cdn_urls_in_templates():
|
||||||
|
"""All .html templates use /static/ paths only — no CDN URLs in href/src attributes."""
|
||||||
|
templates_dir = Path(__file__).parent.parent / "imptune" / "templates"
|
||||||
|
html_files = list(templates_dir.glob("**/*.html"))
|
||||||
|
assert html_files, "No HTML templates found — check templates directory path"
|
||||||
|
|
||||||
|
violations = []
|
||||||
|
for html_file in html_files:
|
||||||
|
content = html_file.read_text(encoding="utf-8")
|
||||||
|
if CDN_PATTERNS.search(content):
|
||||||
|
violations.append(f"{html_file.name}: contains CDN domain reference")
|
||||||
|
if EXTERNAL_URL_PATTERN.search(content):
|
||||||
|
violations.append(f"{html_file.name}: contains external https:// in href/src")
|
||||||
|
|
||||||
|
assert not violations, "CDN/external URL violations found:\n" + "\n".join(violations)
|
||||||
|
|
||||||
|
|
||||||
|
def test_dashboard_returns_200(client):
|
||||||
|
"""GET / returns 200 (dashboard page)."""
|
||||||
|
response = client.get("/")
|
||||||
|
assert response.status_code == 200
|
||||||
Reference in New Issue
Block a user