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