This commit is contained in:
2026-09-09 17:49:01 +01:00
commit 99c15e23bb
30 changed files with 3028 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
import re
from collections.abc import Iterator
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
@pytest.fixture(autouse=True)
def env(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
"""Point settings at a fresh temp data dir and a known secret for every test."""
monkeypatch.setenv("BIKE_ADMIN_DATA_DIR", str(tmp_path / "data"))
monkeypatch.setenv("BIKE_ADMIN_SECRET_KEY", "test-key-1234")
monkeypatch.setenv("BIKE_ADMIN_USERNAME", "admin")
monkeypatch.setenv("BIKE_ADMIN_PASSWORD", "correct-horse-battery-staple")
monkeypatch.setenv("BIKE_ADMIN_LOGIN_MAX_ATTEMPTS", "1000")
monkeypatch.delenv("BIKE_ADMIN_IS_PRODUCTION", raising=False)
from app.config import get_settings
get_settings.cache_clear()
yield
get_settings.cache_clear()
class Client:
"""A TestClient with small helpers for logging in and filling forms."""
PW = "correct-horse-battery-staple"
def __init__(self, raw: TestClient):
self.raw = raw
# --- pass-through HTTP ------------------------------------------------
def get(self, path: str, **kw):
return self.raw.get(path, **kw)
def post(self, path: str, **kw):
return self.raw.post(path, **kw)
# --- auth -------------------------------------------------------------
def login(self, user: str = "admin", pw: str | None = None) -> "object":
r = self.raw.post("/login", data={"username": user, "password": pw or self.PW, "next": "/"})
assert r.status_code == 303, f"login failed: {r.status_code} {r.text[:300]}"
return r
def logout(self):
return self.raw.get("/logout")
# --- helpers ----------------------------------------------------------
@staticmethod
def csrf(html: str) -> str:
m = re.search(r'name="csrf"[^>]*value="([^"]+)"', html)
assert m, "CSRF token not found in page"
return m.group(1)
def create_bike(self, name: str = "Roadie", interval: int | None = None) -> int:
page = self.raw.get("/bikes/new")
data = {"csrf": self.csrf(page.text), "name": name}
if interval is not None:
data["service_interval_miles"] = str(interval)
resp = self.raw.post("/bikes/new", data=data)
assert resp.status_code == 303, resp.text[:500]
m = re.search(r"/bikes/(\d+)", resp.headers["location"])
assert m
return int(m.group(1))
def create_event(self, bike_id: int, **data) -> dict:
page = self.raw.get(f"/bikes/{bike_id}")
payload = {"csrf": self.csrf(page.text)}
payload.update(data)
resp = self.raw.post(f"/bikes/{bike_id}/events", data=payload)
assert resp.status_code in (200, 303), f"{resp.status_code} {resp.text[:500]}"
return {"status": resp.status_code, "text": resp.text}
def delete_event(self, event_id: int) -> "object":
page = self.raw.get("/bikes/1") # just need any page to read the csrf token
resp = self.raw.post(f"/events/{event_id}/delete", data={"csrf": self.csrf(page.text)})
return resp
@pytest.fixture
def client() -> Iterator[Client]:
from app.main import app
raw = TestClient(app, raise_server_exceptions=True, follow_redirects=False)
c = Client(raw)
with raw:
yield c