Files
ohm_streaming/tests/test_api.py
T
Roman 9148b5fb6a Mise à jour automatique (Gitea + Watchtower) et conteneurisation Docker
- Détection de version via l'API Gitea, application via Watchtower
- Router système, UI admin (intégrations), bandeau de mise à jour
- Dockerfile multi-étapes, docker-compose, scripts/release.sh
- Version centralisée dans app/version.py (pyproject ou OHM_VERSION au build)
2026-09-22 11:58:03 +00:00

189 lines
8.1 KiB
Python

"""Tests d'intégration API (auth, downloads, favoris, streaming, admin)."""
from app.config import get_settings
from app.db import db
from app.scrapers.base import ScrapeError, SearchResult
async def test_health(client):
r = await client.get("/health")
assert r.status_code == 200 and r.json() == {"status": "ok"}
async def test_pages_require_auth(client):
r = await client.get("/", follow_redirects=False)
assert r.status_code == 303 and r.headers["location"] == "/login"
r = await client.get("/api/downloads", follow_redirects=False)
assert r.status_code == 303
async def test_register_login_me(client, admin_cookies):
r = await client.get("/auth/me", cookies=admin_cookies)
data = r.json()
assert data["username"] == "admin" and data["is_admin"] is True
async def test_duplicate_username(client):
await client.post("/auth/register", data={"username": "toto", "password": "secret123"})
r = await client.post("/auth/register", data={"username": "toto", "password": "secret123"})
assert r.status_code == 409
async def test_enqueue_bad_internal_url(client, admin_cookies):
r = await client.post(
"/api/downloads", json={"internal_url": "invalide"}, cookies=admin_cookies
)
assert r.status_code == 422
async def test_favorites_flow(client, admin_cookies):
fav = {"source": "vostfree", "source_id": "1-x", "title": "Frieren"}
r = await client.post("/api/favorites", json=fav, cookies=admin_cookies)
assert r.status_code == 201
r = await client.post("/api/favorites", json=fav, cookies=admin_cookies)
assert r.status_code == 409
r = await client.get("/api/favorites", cookies=admin_cookies)
assert r.json()["total"] == 1
fav_id = r.json()["items"][0]["id"]
r = await client.delete(f"/api/favorites/{fav_id}", cookies=admin_cookies)
assert r.status_code == 200
async def test_stream_range(client, admin_cookies):
payload = b"video-bytes" * 1000
path = get_settings().download_dir / "ep1.mp4"
path.write_bytes(payload)
await db.execute(
"INSERT INTO downloads (source_key, video_url, page_url, title, file_path, status, "
"total_bytes, downloaded_bytes) VALUES ('k', 'u', 'p', 'Ep 1', 'ep1.mp4', 'done', ?, ?)",
(len(payload), len(payload)),
)
r = await client.get("/api/stream/1", cookies=admin_cookies)
assert r.status_code == 200 and r.content == payload
r = await client.get("/api/stream/1", headers={"Range": "bytes=10-19"}, cookies=admin_cookies)
assert r.status_code == 206
assert r.content == payload[10:20]
assert r.headers["content-range"] == f"bytes 10-19/{len(payload)}"
r = await client.get(
"/api/stream/1", headers={"Range": "bytes=999999999-"}, cookies=admin_cookies
)
assert r.status_code == 416
async def test_admin_required(client):
await client.post("/auth/register", data={"username": "admin2", "password": "secret123"})
r = await client.post("/auth/register", data={"username": "user", "password": "secret123"})
user_cookies = r.cookies
r = await client.get("/api/admin/users", cookies=user_cookies)
assert r.status_code == 403
async def test_admin_user_management(client, admin_cookies):
await client.post("/auth/register", data={"username": "user", "password": "secret123"})
r = await client.get("/api/admin/users", cookies=admin_cookies)
users = {u["username"]: u["id"] for u in r.json()}
assert set(users) == {"admin", "user"}
admin_id, user_id = users["admin"], users["user"]
r = await client.post(f"/api/admin/users/{user_id}/toggle-active", cookies=admin_cookies)
assert r.json() == {"is_active": False}
r = await client.post(f"/api/admin/users/{user_id}/toggle-admin", cookies=admin_cookies)
assert r.json() == {"is_admin": True}
# pas d'auto-modification
r = await client.post(f"/api/admin/users/{admin_id}/toggle-admin", cookies=admin_cookies)
assert r.status_code == 400
r = await client.delete(f"/api/admin/users/{user_id}", cookies=admin_cookies)
assert r.status_code == 200
async def test_source_toggle(client, admin_cookies):
r = await client.post(
"/api/admin/sources/vostfree/toggle", json={"enabled": False}, cookies=admin_cookies
)
assert r.json()["enabled"] is False
r = await client.get("/api/sources", cookies=admin_cookies)
states = {s["name"]: s["enabled"] for s in r.json()}
assert states["vostfree"] is False
assert states["french_manga"] is True
async def test_source_health(client, admin_cookies, monkeypatch):
# source inconnue → 404 (pas de 500)
r = await client.post("/api/admin/sources/inconnue/health", cookies=admin_cookies)
assert r.status_code == 404
# état initial : aucune santé connue
r = await client.get("/api/admin/sources", cookies=admin_cookies)
assert all(s["health"] is None for s in r.json())
async def ok_search(self, query: str) -> list[SearchResult]:
return [SearchResult(source=self.name, source_id="x", title="X", url="http://x")]
monkeypatch.setattr("app.scrapers.sources.vostfree.VostfreeScraper.search", ok_search)
r = await client.post("/api/admin/sources/vostfree/health", cookies=admin_cookies)
data = r.json()
assert data["healthy"] is True and data["detail"] == "1 résultats" and data["checked_at"]
# résultat persisté et exposé par GET /sources
r = await client.get("/api/admin/sources", cookies=admin_cookies)
health = {s["name"]: s["health"] for s in r.json()}
assert health["vostfree"]["healthy"] is True
assert health["french_manga"] is None
async def failing_search(self, query: str) -> list[SearchResult]:
raise ScrapeError("site indisponible")
monkeypatch.setattr("app.scrapers.sources.vostfree.VostfreeScraper.search", failing_search)
r = await client.post("/api/admin/sources/vostfree/health", cookies=admin_cookies)
data = r.json()
assert r.status_code == 200 and data["healthy"] is False and data["detail"] == "site indisponible"
async def broken_search(self, query: str) -> list[SearchResult]:
raise RuntimeError("boom")
monkeypatch.setattr("app.scrapers.sources.vostfree.VostfreeScraper.search", broken_search)
r = await client.post("/api/admin/sources/vostfree/health", cookies=admin_cookies)
data = r.json()
assert r.status_code == 200 and data["healthy"] is False and "boom" in data["detail"]
async def test_source_toggle_unknown(client, admin_cookies):
r = await client.post(
"/api/admin/sources/inconnue/toggle", json={"enabled": True}, cookies=admin_cookies
)
assert r.status_code == 404
async def test_source_url_override(client, admin_cookies):
from app.scrapers.base import get_source
r = await client.get("/api/admin/sources", cookies=admin_cookies)
src = next(s for s in r.json() if s["name"] == "vostfree")
default = src["default_base_url"]
assert src["base_url"] == default and src["overridden"] is False
# source inconnue → 404
r = await client.put("/api/admin/sources/inconnue/url", json={"url": "https://x.org"}, cookies=admin_cookies)
assert r.status_code == 404
# URL invalide → 422
r = await client.put("/api/admin/sources/vostfree/url", json={"url": "pas-une-url"}, cookies=admin_cookies)
assert r.status_code == 422
# changement de domaine (slash final toléré) → appliqué à l'instance + persisté
r = await client.put("/api/admin/sources/vostfree/url", json={"url": "https://exemple.org/"},
cookies=admin_cookies)
assert r.status_code == 200
data = r.json()
assert data["base_url"] == "https://exemple.org" and data["overridden"] is True
assert get_source("vostfree").base_url == "https://exemple.org"
r = await client.get("/api/admin/sources", cookies=admin_cookies)
src = next(s for s in r.json() if s["name"] == "vostfree")
assert src["base_url"] == "https://exemple.org" and src["overridden"] is True
# URL vide → retour au défaut du code
r = await client.put("/api/admin/sources/vostfree/url", json={"url": ""}, cookies=admin_cookies)
data = r.json()
assert data["base_url"] == default and data["overridden"] is False
assert get_source("vostfree").base_url == default