update
This commit is contained in:
@@ -10,6 +10,7 @@ from pathlib import Path
|
||||
|
||||
_ORDNER = Path(tempfile.mkdtemp(prefix="filmabend-tests-"))
|
||||
os.environ["DB_PATH"] = str(_ORDNER / "test.db")
|
||||
os.environ["CLIPS_DIR"] = str(_ORDNER / "clips")
|
||||
os.environ.setdefault("TMDB_TOKEN", "test")
|
||||
os.environ.setdefault("MINIMAX_KEY", "test")
|
||||
# Der Hostmodus hängt am Schlüssel — in den Tests ein fester
|
||||
@@ -18,7 +19,7 @@ os.environ["HOST_SCHLUESSEL"] = "test-host-schluessel"
|
||||
import pytest # noqa: E402
|
||||
from fastapi.testclient import TestClient # noqa: E402
|
||||
|
||||
from app import db, features, main, suggestions, users, watched, wishlist # noqa: E402
|
||||
from app import clips, db, features, main, suggestions, users, watched, wishlist # noqa: E402
|
||||
|
||||
# Zählt, ob der Startup doch einen Sync anstoßen wollte
|
||||
_SYNC_AUFRUFE = []
|
||||
@@ -49,7 +50,7 @@ def sync_aufrufe():
|
||||
def frische_db(app_start):
|
||||
"""Jeder Test beginnt mit leeren Tabellen und ohne laufende Ziehung."""
|
||||
db.DB_PATH.unlink(missing_ok=True)
|
||||
for modul in (db, users, suggestions, watched, wishlist, features):
|
||||
for modul in (db, users, suggestions, watched, clips, wishlist, features):
|
||||
modul.init()
|
||||
main.letzter_dreh.update(ergebnis=None, start=0.0)
|
||||
|
||||
|
||||
177
backend/tests/test_clips.py
Normal file
177
backend/tests/test_clips.py
Normal file
@@ -0,0 +1,177 @@
|
||||
"""Clips: Marke setzen, Fenster ziehen, schneiden."""
|
||||
|
||||
import httpx
|
||||
|
||||
from app import clips, main, punkte
|
||||
|
||||
|
||||
def marke(client):
|
||||
antwort = client.post("/api/clips")
|
||||
assert antwort.status_code == 200, antwort.text
|
||||
return antwort.json()
|
||||
|
||||
|
||||
def test_marke_ist_nur_ein_zeitpunkt(client, film):
|
||||
"""Ohne Dreh und ohne Abend gehört die Marke keinem Film; das Fenster
|
||||
hat Standardwerte."""
|
||||
m = marke(client)
|
||||
assert m["movie_id"] is None
|
||||
assert (m["vorher"], m["dauer"]) == (clips.VORHER, clips.DAUER)
|
||||
assert m["mine"] is True and m["geschnitten"] is False
|
||||
assert m["name"] == "Anna"
|
||||
|
||||
|
||||
def test_marke_haengt_am_gezogenen_film(client, zuschauer, hostmodus, film):
|
||||
client.post("/api/suggestions", json={"movie_id": film(1)})
|
||||
hostmodus(zuschauer)
|
||||
zuschauer.post("/api/spin")
|
||||
m = marke(client)
|
||||
assert m["movie_id"] == film(1)
|
||||
assert m["title"] == "Film 1"
|
||||
main.letzter_dreh["ergebnis"] = None
|
||||
zuschauer.delete("/api/suggestions/alle")
|
||||
|
||||
|
||||
def test_ohne_dreh_zaehlt_der_neueste_abend(client, film):
|
||||
client.post("/api/watched", json={"movie_id": film(1)})
|
||||
client.post("/api/watched", json={"movie_id": film(2)})
|
||||
assert marke(client)["movie_id"] == film(2)
|
||||
|
||||
|
||||
def test_tage_mit_aufnahme(client, monkeypatch):
|
||||
async def segmente(von, bis):
|
||||
# Ein Abend mit Stück vor und nach Mitternacht, ein weiterer am Tag danach
|
||||
return [
|
||||
{"start": "2026-09-23T20:00:00Z", "duration": 30},
|
||||
{"start": "2026-09-24T00:30:00Z", "duration": 30},
|
||||
{"start": "2026-09-25T19:00:00Z", "duration": 30},
|
||||
]
|
||||
|
||||
monkeypatch.setattr(clips, "_segmente", segmente)
|
||||
assert client.get("/api/clips/aufnahmen").json()["tage"] == ["2026-09-23", "2026-09-25"]
|
||||
|
||||
|
||||
def test_clip_zu_altem_abend(client, film, monkeypatch):
|
||||
from datetime import datetime, timezone
|
||||
|
||||
client.post("/api/watched", json={"movie_id": film(4)})
|
||||
# Kein Abend zu diesem Film
|
||||
assert client.post("/api/clips/von-film", json={"movie_id": 999}).status_code == 404
|
||||
|
||||
# Keine Aufnahme an dem Tag
|
||||
async def keine(watched_at):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(clips, "aufnahme_des_abends", keine)
|
||||
antwort = client.post("/api/clips/von-film", json={"movie_id": film(4)})
|
||||
assert antwort.status_code == 404 and "Aufnahme" in antwort.json()["detail"]
|
||||
|
||||
# Mit Aufnahme: die Marke liegt kurz nach ihrem Beginn
|
||||
beginn = datetime(2026, 9, 23, 19, 0, tzinfo=timezone.utc)
|
||||
|
||||
async def da(watched_at):
|
||||
return beginn
|
||||
|
||||
monkeypatch.setattr(clips, "aufnahme_des_abends", da)
|
||||
m = client.post("/api/clips/von-film", json={"movie_id": film(4)}).json()
|
||||
assert m["movie_id"] == film(4)
|
||||
assert m["marke_at"] == "2026-09-23T19:01:00+00:00"
|
||||
|
||||
|
||||
def test_fremde_sehen_nichts_der_host_alles(client, client2, zuschauer, hostmodus):
|
||||
m = marke(client)
|
||||
assert [c for c in client2.get("/api/clips").json() if c["id"] == m["id"]] == []
|
||||
assert client2.patch(f"/api/clips/{m['id']}", json={"titel": "x"}).status_code == 404
|
||||
assert client2.get(f"/api/clips/{m['id']}/video").status_code == 404
|
||||
hostmodus(zuschauer)
|
||||
assert zuschauer.patch(f"/api/clips/{m['id']}", json={"titel": "Vom Host"}).json()["titel"] == "Vom Host"
|
||||
assert zuschauer.get("/api/clips").json()[0]["id"] == m["id"]
|
||||
|
||||
|
||||
def test_fenster_wird_begrenzt_und_macht_schnitt_ungueltig(client, monkeypatch):
|
||||
m = marke(client)
|
||||
geaendert = client.patch(f"/api/clips/{m['id']}", json={"vorher": 5000, "dauer": 1, "titel": " Hallo "}).json()
|
||||
assert (geaendert["vorher"], geaendert["dauer"], geaendert["titel"]) == (clips.MAX_VORHER, clips.MIN_DAUER, "Hallo")
|
||||
|
||||
# Schneiden: der Playback-Server wird nachgestellt
|
||||
class Antwort:
|
||||
status_code = 200
|
||||
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
async def aiter_bytes(self):
|
||||
yield b"mp4-daten"
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
pass
|
||||
|
||||
aufrufe = []
|
||||
|
||||
class Client:
|
||||
def __init__(self, **kw):
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
pass
|
||||
|
||||
def stream(self, methode, url, params=None):
|
||||
aufrufe.append(params)
|
||||
return Antwort()
|
||||
|
||||
monkeypatch.setattr(clips.httpx, "AsyncClient", Client)
|
||||
fertig = client.post(f"/api/clips/{m['id']}/schneiden").json()
|
||||
assert fertig["geschnitten"] is True
|
||||
assert aufrufe[0]["path"] == "filmabend" and aufrufe[0]["duration"] == clips.MIN_DAUER
|
||||
assert (clips.CLIPS_DIR / f"{m['id']}.mp4").read_bytes() == b"mp4-daten"
|
||||
video = client.get(f"/api/clips/{m['id']}/video")
|
||||
assert video.status_code == 200 and video.content == b"mp4-daten"
|
||||
|
||||
# Neues Fenster: Datei weg, muss neu geschnitten werden
|
||||
neu = client.patch(f"/api/clips/{m['id']}", json={"dauer": 20}).json()
|
||||
assert neu["geschnitten"] is False
|
||||
assert not (clips.CLIPS_DIR / f"{m['id']}.mp4").exists()
|
||||
assert client.get(f"/api/clips/{m['id']}/video").status_code == 404
|
||||
|
||||
|
||||
def test_schneiden_ohne_aufnahme_meldet_502(client, monkeypatch):
|
||||
m = marke(client)
|
||||
|
||||
class Client:
|
||||
def __init__(self, **kw):
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
pass
|
||||
|
||||
def stream(self, *a, **kw):
|
||||
raise httpx.ConnectError("weg")
|
||||
|
||||
monkeypatch.setattr(clips.httpx, "AsyncClient", Client)
|
||||
assert client.post(f"/api/clips/{m['id']}/schneiden").status_code == 502
|
||||
|
||||
|
||||
def test_loeschen_nimmt_die_datei_mit(client):
|
||||
m = marke(client)
|
||||
clips.CLIPS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
(clips.CLIPS_DIR / f"{m['id']}.mp4").write_bytes(b"x")
|
||||
from app import db
|
||||
|
||||
with db.connect() as conn:
|
||||
conn.execute("UPDATE clips SET datei = ? WHERE id = ?", (f"{m['id']}.mp4", m["id"]))
|
||||
assert client.delete(f"/api/clips/{m['id']}").json()["removed"] == 1
|
||||
assert not (clips.CLIPS_DIR / f"{m['id']}.mp4").exists()
|
||||
assert client.get("/api/clips").json() == []
|
||||
|
||||
|
||||
def test_zuschauer_darf_keine_marke_setzen(zuschauer):
|
||||
assert zuschauer.post("/api/clips").status_code == 403
|
||||
@@ -183,3 +183,24 @@ def test_ausschalten_geht_ohne_schluessel(zuschauer, hostmodus):
|
||||
def test_der_schluessel_gilt_nur_fuer_die_eigene_sitzung(zuschauer, client2, hostmodus):
|
||||
hostmodus(zuschauer)
|
||||
assert client2.get("/api/host").json()["host"] is False
|
||||
|
||||
|
||||
def test_hostfilm_oeffnet_den_hostmodus(zuschauer, client, hostmodus, film):
|
||||
# Nur der Host darf den Host-Film setzen oder sehen
|
||||
assert client.post("/api/host/film", json={"movie_id": film(1)}).status_code == 403
|
||||
assert client.get("/api/host/film").status_code == 403
|
||||
hostmodus(zuschauer)
|
||||
assert zuschauer.post("/api/host/film", json={"movie_id": film(1)}).json()["gesetzt"] is True
|
||||
assert zuschauer.get("/api/host/film").json()["film"]["id"] == film(1)
|
||||
|
||||
# Falscher Film: nichts; richtiger Film: Host
|
||||
assert client.post("/api/host", json={"an": True, "movie_id": film(2)}).status_code == 403
|
||||
assert client.get("/api/host").json()["host"] is False
|
||||
assert client.post("/api/host", json={"an": True, "movie_id": film(1)}).json()["host"] is True
|
||||
assert client.get("/api/host").json()["host"] is True
|
||||
|
||||
# Aufheben: der Film öffnet nichts mehr
|
||||
zuschauer.post("/api/host/film", json={"movie_id": None})
|
||||
assert zuschauer.get("/api/host/film").json()["film"] is None
|
||||
client.post("/api/host", json={"an": False})
|
||||
assert client.post("/api/host", json={"an": True, "movie_id": film(1)}).status_code == 403
|
||||
|
||||
Reference in New Issue
Block a user