From 34c7cb30f123e39dbe5710094d073285f77c807b Mon Sep 17 00:00:00 2001 From: Kawa Date: Fri, 10 Apr 2026 11:26:48 +0200 Subject: [PATCH] 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 --- imptune/api/pages.py | 6 +++--- imptune/main.py | 20 +++++++++++--------- requirements-dev.txt | 2 ++ tests/conftest.py | 26 ++++++++++++++++++++++++++ tests/test_health.py | 5 +++++ tests/test_static.py | 37 +++++++++++++++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 12 deletions(-) create mode 100644 requirements-dev.txt create mode 100644 tests/conftest.py create mode 100644 tests/test_health.py create mode 100644 tests/test_static.py diff --git a/imptune/api/pages.py b/imptune/api/pages.py index 344b10c..9027fcb 100644 --- a/imptune/api/pages.py +++ b/imptune/api/pages.py @@ -11,9 +11,9 @@ templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templa @router.get("/", response_class=HTMLResponse) def dashboard(request: Request): return templates.TemplateResponse( - "dashboard.html", - { - "request": request, + request=request, + name="dashboard.html", + context={ "recent_printers": [], "recent_packages": [], }, diff --git a/imptune/main.py b/imptune/main.py index 3da307d..55ca869 100644 --- a/imptune/main.py +++ b/imptune/main.py @@ -1,3 +1,5 @@ +import os +from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI @@ -6,7 +8,15 @@ from fastapi.staticfiles import StaticFiles from imptune.api import health, pages 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) _static_dir = Path(__file__).parent / "static" @@ -15,11 +25,3 @@ app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static") # Register routers app.include_router(health.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) diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..719f860 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,2 @@ +pytest>=8.0 +httpx>=0.27 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..dbe17c3 --- /dev/null +++ b/tests/conftest.py @@ -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 diff --git a/tests/test_health.py b/tests/test_health.py new file mode 100644 index 0000000..01f7cbc --- /dev/null +++ b/tests/test_health.py @@ -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"} diff --git a/tests/test_static.py b/tests/test_static.py new file mode 100644 index 0000000..1cbeb9e --- /dev/null +++ b/tests/test_static.py @@ -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