Files
OAMC-Auswertung/tests/test_api_ingest.py
T
linusandClaude Sonnet 5 03f6fdb0d9 Erstumsetzung: OAMC Turnierauswertung (AP 0–7, 9)
* Datenmodell + Alembic-Initialmigration (Plan §4), Seed Saison 2026
* Wertungs-Engine (Plan §3.3), regelbasiert je Kategorie/Saison
  - Abnahmetest gegen die Ergebnisliste 2026-05-31
* Meisterschaftsberechnung mit Streichresultaten + Tie-Break
  (PLATZHALTER-Punktetabelle, O-1 offen)
* Ingest-API (idempotent, dry_run) inkl. Excel-Leser und Fahrer-Matching/Merge
* Zeitmessungs-Endpoint (Batch, idempotent, unzugeordnet/unplausibel) + SSE-Anzeige
  - Mess-Pi-Client bewusst NICHT enthalten (docs/zeitmessung-endpoint.md)
* Frontend: öffentliche Seiten + internes Backend (Jinja2/HTMX, kein Build)
* Auslieferung: Docker (multi-arch) + docker-compose sowie Debian-Paket/APT-Repo
* 57 Tests grün gegen echte PostgreSQL-Testdatenbank

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-05 18:49:00 +02:00

105 lines
4.1 KiB
Python

"""Ingest-API (Plan §6.1): Auth, dry_run, Idempotenz, Nachrechnung."""
from sqlalchemy import select
from oamc.models import Ergebnis, Start, Turnier
from oamc.models.enums import VeranstaltungStatus
from oamc.seeds import seed_saison_2026
from tests import factories as f
HEAD = {"X-API-Key": "test-key"}
def _turnier(client):
db = client.db
seed_saison_2026(db)
v = f.veranstaltung(db, status=VeranstaltungStatus.LAUFEND)
t = f.turnier(db, v)
db.flush()
return t.id
def test_ohne_api_key_401(client):
r = client.post("/api/v1/turniere/1/ergebnisse", json={"quelle": "x", "idempotenz_schluessel": "k", "ergebnisse": []})
assert r.status_code == 401
def test_dry_run_meldet_neue_fahrer_ohne_zu_schreiben(client):
tid = _turnier(client)
payload = {
"quelle": "excel-auswertung-v3",
"idempotenz_schluessel": "2026-05-31-reinheim-final",
"ergebnisse": [
{
"startnummer": 32,
"fahrer": {"nachname": "Gál-Szász", "vorname": "Péter", "geburtsjahr": 2015,
"geschlecht": "M", "verein": "OAMC Reinheim"},
"klasse": "J 2", "summe_zeit": "159.16", "summe_fehler": 10,
}
],
}
r = client.post(f"/api/v1/turniere/{tid}/ergebnisse?dry_run=true", json=payload, headers=HEAD)
assert r.status_code == 200
body = r.json()
assert body["dry_run"] is True
assert body["neue_fahrer"] == 1
# nichts geschrieben
assert client.db.scalars(select(Start).where(Start.turnier_id == tid)).first() is None
def test_echter_upload_dann_idempotent(client):
tid = _turnier(client)
payload = {
"quelle": "excel-auswertung-v3",
"idempotenz_schluessel": "abc-final",
"ergebnisse": [
{"startnummer": 1, "fahrer": {"nachname": "Bernius", "vorname": "Meik", "geburtsjahr": 1985},
"klasse": "S 6", "wertungsklasse": "S 5, S 9, S 3, S 6, S 7",
"summe_zeit": "105.43", "summe_fehler": 0,
"gesamt_erwartet": "105.43", "platzierung_erwartet": 1},
{"startnummer": 2, "fahrer": {"nachname": "Schmunk", "vorname": "Christian", "geburtsjahr": 1988},
"klasse": "S 9", "wertungsklasse": "S 5, S 9, S 3, S 6, S 7",
"summe_zeit": "103.26", "summe_fehler": 10,
"gesamt_erwartet": "113.26", "platzierung_erwartet": 2},
],
}
r1 = client.post(f"/api/v1/turniere/{tid}/ergebnisse", json=payload, headers=HEAD)
assert r1.status_code == 200, r1.text
b1 = r1.json()
assert b1["angelegt"] == 2
assert b1["abweichungen"] == [] # Nachrechnung deckt sich mit gesamt_erwartet
r2 = client.post(f"/api/v1/turniere/{tid}/ergebnisse", json=payload, headers=HEAD)
assert r2.json()["idempotent_uebersprungen"] is True
assert client.db.scalars(select(Start).where(Start.turnier_id == tid)).all().__len__() == 2
def test_abweichung_zur_erwartung_wird_gemeldet(client):
tid = _turnier(client)
payload = {
"quelle": "excel", "idempotenz_schluessel": "abw-1",
"ergebnisse": [
{"startnummer": 1, "fahrer": {"nachname": "A", "vorname": "A", "geburtsjahr": 1985},
"klasse": "S 6", "summe_zeit": "100.00", "summe_fehler": 0,
"gesamt_erwartet": "999.00", "platzierung_erwartet": 1},
],
}
r = client.post(f"/api/v1/turniere/{tid}/ergebnisse", json=payload, headers=HEAD)
body = r.json()
assert any(a["feld"] == "gesamt" for a in body["abweichungen"])
assert body["warnungen"]
def test_finalisieren_setzt_status_final(client):
tid = _turnier(client)
client.post(f"/api/v1/turniere/{tid}/ergebnisse", json={
"quelle": "x", "idempotenz_schluessel": "f1",
"ergebnisse": [
{"startnummer": 1, "fahrer": {"nachname": "A", "vorname": "A", "geburtsjahr": 1985},
"klasse": "S 6", "summe_zeit": "100.00", "summe_fehler": 0},
],
}, headers=HEAD)
r = client.post(f"/api/v1/turniere/{tid}/finalisieren", headers=HEAD)
assert r.status_code == 200
assert client.db.get(Turnier, tid).status.value == "final"