import importlib import os import time from typing import Iterator import pymysql import pytest from fastapi.testclient import TestClient def wait_for_db(host: str, port: int, user: str, password: str, timeout_seconds: int = 60) -> None: deadline = time.time() + timeout_seconds while time.time() < deadline: try: conn = pymysql.connect(host=host, port=port, user=user, password=password, autocommit=True) conn.close() return except Exception: time.sleep(1) raise RuntimeError("MariaDB did not become ready in time") @pytest.fixture(scope="session") def app_module(): os.environ.setdefault("DB_HOST", "127.0.0.1") os.environ.setdefault("DB_PORT", "3306") os.environ.setdefault("DB_USER", "jellomator") os.environ.setdefault("DB_PASSWORD", "jellomator") os.environ.setdefault("DB_NAME", "jellomator_test") wait_for_db( host=os.environ["DB_HOST"], port=int(os.environ["DB_PORT"]), user=os.environ["DB_USER"], password=os.environ["DB_PASSWORD"], ) module = importlib.import_module("backend.main") return module @pytest.fixture() def client(app_module) -> Iterator[TestClient]: with app_module.db() as conn: with conn.cursor() as cur: cur.execute("delete from sessions") cur.execute("delete from users") cur.execute("delete from links") app_module.login_attempts.clear() app_module.login_lockouts.clear() with TestClient(app_module.app) as test_client: yield test_client def test_healthz(client: TestClient): resp = client.get("/healthz") assert resp.status_code == 200 assert resp.json() == {"ok": True} def test_readyz(client: TestClient): resp = client.get("/readyz") assert resp.status_code == 200 assert resp.json() == {"ok": True} def test_setup_and_login(client: TestClient): setup_resp = client.post("/api/setup", json={"username": "admin", "password": "123456789012"}) assert setup_resp.status_code == 200 assert setup_resp.json() == {"ok": True} login_resp = client.post("/api/login", json={"username": "admin", "password": "123456789012"}) assert login_resp.status_code == 200 assert login_resp.json() == {"ok": True} assert "jellomator_session=" in login_resp.headers.get("set-cookie", "") def test_link_crud_with_auth(client: TestClient): client.post("/api/setup", json={"username": "admin", "password": "123456789012"}) login_resp = client.post("/api/login", json={"username": "admin", "password": "123456789012"}) assert login_resp.status_code == 200 create_resp = client.post( "/api/links", data={ "name": "Prowlarr", "url": "https://prowlarr.example.com", "description": "Indexer manager", "category": "Arr", "enabled": "true", }, ) assert create_resp.status_code == 200 assert create_resp.json() == {"ok": True} list_resp = client.get("/api/links") assert list_resp.status_code == 200 rows = list_resp.json() assert len(rows) == 1 assert rows[0]["name"] == "Prowlarr" link_id = rows[0]["id"] patch_resp = client.patch( f"/api/links/{link_id}", data={ "name": "Prowlarr Updated", "url": "https://prowlarr.example.com/new", "description": "Updated", "category": "Arr", "enabled": "true", }, ) assert patch_resp.status_code == 200 assert patch_resp.json() == {"ok": True} delete_resp = client.delete(f"/api/links/{link_id}") assert delete_resp.status_code == 200 assert delete_resp.json() == {"ok": True} def test_login_rate_limit_lockout(client: TestClient): client.post("/api/setup", json={"username": "admin", "password": "123456789012"}) for _ in range(5): resp = client.post("/api/login", json={"username": "admin", "password": "wrong-password"}) assert resp.status_code == 401 locked = client.post("/api/login", json={"username": "admin", "password": "wrong-password"}) assert locked.status_code == 429