89 lines
3.3 KiB
Python
89 lines
3.3 KiB
Python
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
|