Batman
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user