2025-08-11 19:40:32 +02:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
"""
|
2025-08-11 19:46:53 +02:00
|
|
|
|
Erweiterter API-Schnelltest - v2.1
|
2025-08-11 19:40:32 +02:00
|
|
|
|
|
2025-08-11 19:44:39 +02:00
|
|
|
|
Änderungen ggü. v2.0:
|
|
|
|
|
|
- **Login VOR** /semantic/pages (viele Wikis liefern Ask-Ergebnisse nur authentifiziert)
|
|
|
|
|
|
- Robuster Fallback: Wenn /semantic/pages {} liefert → optionaler ENV-Fallback `TEST_WIKI_TITLE`
|
|
|
|
|
|
- Bessere Diagnoseausgaben (zeigt Basis-URL, Category, gesetzte Login-ENV)
|
2025-08-11 19:40:32 +02:00
|
|
|
|
|
2025-08-11 19:44:39 +02:00
|
|
|
|
ENV:
|
2025-08-11 19:40:32 +02:00
|
|
|
|
BASE_URL (default http://127.0.0.1:8000)
|
|
|
|
|
|
TEST_WIKI_CATEGORY (default "Übungen")
|
2025-08-11 19:44:39 +02:00
|
|
|
|
TEST_WIKI_TITLE (optional Fallback-Titel, z. B. "Affenklatschen")
|
|
|
|
|
|
WIKI_BOT_USER, WIKI_BOT_PASSWORD (optional; empfohlen)
|
2025-08-11 19:40:32 +02:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import json
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
|
|
BASE = os.getenv("BASE_URL", "http://127.0.0.1:8000").rstrip("/")
|
|
|
|
|
|
TEST_WIKI_CATEGORY = os.getenv("TEST_WIKI_CATEGORY", "Übungen")
|
2025-08-11 19:44:39 +02:00
|
|
|
|
TEST_WIKI_TITLE = os.getenv("TEST_WIKI_TITLE")
|
2025-08-11 19:40:32 +02:00
|
|
|
|
WIKI_USER = os.getenv("WIKI_BOT_USER")
|
|
|
|
|
|
WIKI_PASS = os.getenv("WIKI_BOT_PASSWORD")
|
|
|
|
|
|
|
2025-08-11 19:44:39 +02:00
|
|
|
|
print("\nConfig:")
|
|
|
|
|
|
print(" BASE_URL=", BASE)
|
|
|
|
|
|
print(" TEST_WIKI_CATEGORY=", TEST_WIKI_CATEGORY)
|
|
|
|
|
|
print(" TEST_WIKI_TITLE=", TEST_WIKI_TITLE or "<none>")
|
|
|
|
|
|
print(" WIKI_BOT_USER set=", bool(WIKI_USER))
|
2025-08-11 19:40:32 +02:00
|
|
|
|
|
|
|
|
|
|
# ---- helpers ----
|
|
|
|
|
|
|
|
|
|
|
|
def fail(msg, resp: requests.Response | None = None):
|
|
|
|
|
|
print("✗", msg)
|
|
|
|
|
|
if resp is not None:
|
|
|
|
|
|
try:
|
|
|
|
|
|
print(" → status:", resp.status_code)
|
|
|
|
|
|
print(" → body:", resp.text[:800])
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ok(msg):
|
|
|
|
|
|
print("✓", msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- basic ----
|
|
|
|
|
|
|
|
|
|
|
|
def test_openapi():
|
|
|
|
|
|
r = requests.get(f"{BASE}/openapi.json")
|
|
|
|
|
|
if r.status_code != 200:
|
|
|
|
|
|
fail("/openapi.json nicht erreichbar", r)
|
|
|
|
|
|
ok("OpenAPI erreichbar")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- wiki ----
|
|
|
|
|
|
|
|
|
|
|
|
def test_wiki_health():
|
|
|
|
|
|
r = requests.get(f"{BASE}/import/wiki/health", params={"verbose": 1})
|
|
|
|
|
|
if r.status_code != 200 or r.json().get("status") != "ok":
|
|
|
|
|
|
fail("/import/wiki/health fehlgeschlagen", r)
|
|
|
|
|
|
ok("Wiki /health ok")
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-08-11 19:44:39 +02:00
|
|
|
|
def test_wiki_login_if_env():
|
|
|
|
|
|
if not (WIKI_USER and WIKI_PASS):
|
|
|
|
|
|
print("⚠️ Wiki /login übersprungen (WIKI_BOT_USER/PASSWORD nicht gesetzt)")
|
|
|
|
|
|
return False
|
|
|
|
|
|
r = requests.post(f"{BASE}/import/wiki/login", json={"username": WIKI_USER, "password": WIKI_PASS})
|
|
|
|
|
|
if r.status_code != 200 or r.json().get("status") != "success":
|
|
|
|
|
|
fail("/import/wiki/login fehlgeschlagen", r)
|
|
|
|
|
|
ok("Wiki /login ok")
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_wiki_title_and_pageid() -> tuple[str, int]:
|
|
|
|
|
|
# 1) Versuch über semantic/pages
|
2025-08-11 19:40:32 +02:00
|
|
|
|
r = requests.get(f"{BASE}/import/wiki/semantic/pages", params={"category": TEST_WIKI_CATEGORY})
|
|
|
|
|
|
if r.status_code != 200:
|
|
|
|
|
|
fail("/import/wiki/semantic/pages fehlgeschlagen", r)
|
|
|
|
|
|
data = r.json()
|
2025-08-11 19:44:39 +02:00
|
|
|
|
if isinstance(data, dict) and data:
|
|
|
|
|
|
title = next(iter(data.keys()))
|
|
|
|
|
|
entry = data[title]
|
|
|
|
|
|
pid = entry.get("pageid")
|
|
|
|
|
|
if not pid:
|
|
|
|
|
|
fail("/semantic/pages ohne pageid nach Enrichment", r)
|
|
|
|
|
|
ok(f"Wiki /semantic/pages ok – Beispiel: '{title}' (pageid={pid})")
|
|
|
|
|
|
return title, int(pid)
|
|
|
|
|
|
|
|
|
|
|
|
print("ℹ️ /semantic/pages lieferte keine Titel ({}). Versuche Fallback...")
|
|
|
|
|
|
# 2) Fallback über TEST_WIKI_TITLE
|
|
|
|
|
|
if not TEST_WIKI_TITLE:
|
|
|
|
|
|
fail("Kein TEST_WIKI_TITLE gesetzt und /semantic/pages leer – kann keinen Titel testen.")
|
|
|
|
|
|
r2 = requests.get(f"{BASE}/import/wiki/info", params={"title": TEST_WIKI_TITLE})
|
|
|
|
|
|
if r2.status_code != 200:
|
|
|
|
|
|
fail("/import/wiki/info(Fallback) fehlgeschlagen", r2)
|
|
|
|
|
|
js = r2.json()
|
|
|
|
|
|
pid = js.get("pageid")
|
|
|
|
|
|
if not pid:
|
|
|
|
|
|
fail("/info(Fallback) ohne pageid", r2)
|
|
|
|
|
|
ok(f"Wiki /info Fallback ok – '{TEST_WIKI_TITLE}' (pageid={pid})")
|
|
|
|
|
|
return TEST_WIKI_TITLE, int(pid)
|
2025-08-11 19:40:32 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_wiki_info(title: str):
|
|
|
|
|
|
r = requests.get(f"{BASE}/import/wiki/info", params={"title": title})
|
|
|
|
|
|
if r.status_code != 200:
|
|
|
|
|
|
fail("/import/wiki/info fehlgeschlagen", r)
|
|
|
|
|
|
js = r.json()
|
|
|
|
|
|
if not js.get("pageid"):
|
|
|
|
|
|
fail("/info ohne pageid", r)
|
|
|
|
|
|
ok("Wiki /info ok")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_wiki_parse(pageid: int, title: str):
|
|
|
|
|
|
r = requests.get(f"{BASE}/import/wiki/parsepage", params={"pageid": pageid, "title": title})
|
|
|
|
|
|
if r.status_code != 200:
|
|
|
|
|
|
fail("/import/wiki/parsepage fehlgeschlagen", r)
|
|
|
|
|
|
js = r.json()
|
|
|
|
|
|
if not isinstance(js.get("wikitext"), str):
|
|
|
|
|
|
fail("/parsepage ohne wikitext", r)
|
|
|
|
|
|
ok("Wiki /parsepage ok")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---- exercise ----
|
|
|
|
|
|
|
|
|
|
|
|
def make_exercise_payload(external_id: str):
|
|
|
|
|
|
return {
|
|
|
|
|
|
"external_id": external_id,
|
|
|
|
|
|
"fingerprint": "unit-test-sha",
|
|
|
|
|
|
"source": "UnitTest",
|
|
|
|
|
|
"title": "Testübung Reaktion",
|
|
|
|
|
|
"summary": "Kurzbeschreibung für Test.",
|
|
|
|
|
|
"short_description": "Kurzbeschreibung für Test.",
|
|
|
|
|
|
"keywords": ["Reaktion", "Bälle", "Bälle"],
|
|
|
|
|
|
"link": "http://example.local",
|
|
|
|
|
|
"discipline": "Karate",
|
|
|
|
|
|
"group": "5",
|
|
|
|
|
|
"age_group": "Teenager",
|
|
|
|
|
|
"target_group": "Breitensport",
|
|
|
|
|
|
"min_participants": 1,
|
|
|
|
|
|
"duration_minutes": 10,
|
|
|
|
|
|
"capabilities": {"Reaktionsfähigkeit": 2, "Kopplungsfähigkeit": 1},
|
|
|
|
|
|
"category": "Übungen",
|
|
|
|
|
|
"purpose": "Aufwärmen",
|
|
|
|
|
|
"execution": "Einfacher Ablauf.",
|
|
|
|
|
|
"notes": "Hinweise.",
|
|
|
|
|
|
"preparation": "Bälle holen.",
|
|
|
|
|
|
"method": "Frontale Methode",
|
|
|
|
|
|
"equipment": ["Bälle", "Pratze"]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_exercise_upsert_and_idempotence():
|
2025-08-11 19:44:39 +02:00
|
|
|
|
import time
|
2025-08-11 19:40:32 +02:00
|
|
|
|
ext = f"ut:{uuid.uuid4()}"
|
|
|
|
|
|
payload = make_exercise_payload(ext)
|
|
|
|
|
|
|
|
|
|
|
|
# create
|
|
|
|
|
|
r1 = requests.post(f"{BASE}/exercise", json=payload)
|
|
|
|
|
|
if r1.status_code != 200:
|
|
|
|
|
|
fail("POST /exercise (create) fehlgeschlagen", r1)
|
|
|
|
|
|
id1 = r1.json().get("id")
|
|
|
|
|
|
if not id1:
|
|
|
|
|
|
fail("POST /exercise lieferte keine id", r1)
|
|
|
|
|
|
|
|
|
|
|
|
# update (idempotent)
|
|
|
|
|
|
r2 = requests.post(f"{BASE}/exercise", json=payload)
|
|
|
|
|
|
if r2.status_code != 200:
|
|
|
|
|
|
fail("POST /exercise (update) fehlgeschlagen", r2)
|
|
|
|
|
|
id2 = r2.json().get("id")
|
|
|
|
|
|
if id2 != id1:
|
|
|
|
|
|
fail("Idempotenz verletzt: id hat sich geändert")
|
|
|
|
|
|
|
|
|
|
|
|
# by-external-id
|
|
|
|
|
|
r3 = requests.get(f"{BASE}/exercise/by-external-id", params={"external_id": ext})
|
|
|
|
|
|
if r3.status_code != 200:
|
|
|
|
|
|
fail("GET /exercise/by-external-id fehlgeschlagen", r3)
|
|
|
|
|
|
id3 = r3.json().get("id")
|
|
|
|
|
|
if id3 != id1:
|
|
|
|
|
|
fail("Lookup by external_id liefert andere id")
|
|
|
|
|
|
|
|
|
|
|
|
ok("Exercise Upsert & Idempotenz ok")
|
|
|
|
|
|
return ext, id1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_exercise_search_filter(ext_id: str):
|
|
|
|
|
|
req = {
|
|
|
|
|
|
"discipline": "Karate",
|
|
|
|
|
|
"equipment_all": ["Bälle"],
|
|
|
|
|
|
"capability_names": ["Reaktionsfähigkeit"],
|
|
|
|
|
|
"capability_ge_level": 2,
|
|
|
|
|
|
"limit": 10,
|
|
|
|
|
|
}
|
|
|
|
|
|
r = requests.post(f"{BASE}/exercise/search", json=req)
|
|
|
|
|
|
if r.status_code != 200:
|
|
|
|
|
|
fail("POST /exercise/search (Filter) fehlgeschlagen", r)
|
|
|
|
|
|
js = r.json(); hits = js.get("hits", [])
|
|
|
|
|
|
if not isinstance(hits, list) or not hits:
|
|
|
|
|
|
fail("/exercise/search liefert keine Treffer (Filter)", r)
|
|
|
|
|
|
if not any(h.get("payload", {}).get("external_id") == ext_id for h in hits):
|
2025-08-11 19:44:39 +02:00
|
|
|
|
ok("Exercise Search (Filter) ok – Testpunkt nicht zwingend in Top-N")
|
2025-08-11 19:40:32 +02:00
|
|
|
|
else:
|
|
|
|
|
|
ok("Exercise Search (Filter) ok – Testpunkt gefunden")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_exercise_search_vector():
|
|
|
|
|
|
req = {
|
|
|
|
|
|
"query": "Aufwärmen 10min, Reaktionsfähigkeit, Teenager, Bälle",
|
|
|
|
|
|
"discipline": "Karate",
|
|
|
|
|
|
"limit": 5
|
|
|
|
|
|
}
|
|
|
|
|
|
r = requests.post(f"{BASE}/exercise/search", json=req)
|
|
|
|
|
|
if r.status_code != 200:
|
|
|
|
|
|
fail("POST /exercise/search (Vector) fehlgeschlagen", r)
|
|
|
|
|
|
js = r.json(); hits = js.get("hits", [])
|
|
|
|
|
|
if not isinstance(hits, list):
|
|
|
|
|
|
fail("/exercise/search (Vector) liefert ungültige Struktur", r)
|
|
|
|
|
|
ok(f"Exercise Search (Vector) ok – {len(hits)} Treffer")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_exercise_delete(ext_id: str):
|
|
|
|
|
|
r = requests.delete(f"{BASE}/exercise/delete-by-external-id", params={"external_id": ext_id})
|
|
|
|
|
|
if r.status_code != 200:
|
|
|
|
|
|
fail("DELETE /exercise/delete-by-external-id fehlgeschlagen", r)
|
|
|
|
|
|
ok("Exercise Delete-by-external-id ok")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
print("\nStarte API-Tests...\n")
|
|
|
|
|
|
test_openapi()
|
|
|
|
|
|
test_wiki_health()
|
2025-08-11 19:44:39 +02:00
|
|
|
|
|
|
|
|
|
|
# Login nun VOR Semantic Query
|
|
|
|
|
|
did_login = test_wiki_login_if_env()
|
|
|
|
|
|
|
|
|
|
|
|
# Titel ermitteln (semantic/pages oder Fallback)
|
|
|
|
|
|
title, pageid = get_wiki_title_and_pageid()
|
2025-08-11 19:40:32 +02:00
|
|
|
|
test_wiki_info(title)
|
|
|
|
|
|
test_wiki_parse(pageid, title)
|
|
|
|
|
|
|
|
|
|
|
|
# Exercise
|
|
|
|
|
|
ext, _id = test_exercise_upsert_and_idempotence()
|
|
|
|
|
|
test_exercise_search_filter(ext)
|
|
|
|
|
|
test_exercise_search_vector()
|
|
|
|
|
|
test_exercise_delete(ext)
|
|
|
|
|
|
|
|
|
|
|
|
print("\n🎉 Alle Tests erfolgreich durchlaufen!\n")
|