#!/usr/bin/env python3 """API-Test fuer den Trainingsplan-Server (End-to-End, offener Modus). Lauf: python3 tests/test_api.py Voraussetzung: Server-Code liegt eine Ebene ueber tests/. """ import json import os import shutil import subprocess import sys import time import urllib.request import urllib.error import urllib.parse BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) STATE = "/tmp/tp_state" PORT = 8211 BASE_URL = f"http://127.0.0.1:{PORT}" PASS = 0 FAIL = 0 def check(name, cond, detail=""): global PASS, FAIL if cond: PASS += 1 print(f" ok {name}") else: FAIL += 1 print(f"FAIL {name} {detail}") def req(method, path, body=None, token=None): url = BASE_URL + path data = None headers = {} if body is not None: data = json.dumps(body).encode("utf-8") headers["Content-Type"] = "application/json" if token: headers["Authorization"] = "Bearer " + token r = urllib.request.Request(url, data=data, headers=headers, method=method) try: with urllib.request.urlopen(r, timeout=5) as resp: return resp.status, json.loads(resp.read().decode("utf-8")) except urllib.error.HTTPError as e: try: return e.code, json.loads(e.read().decode("utf-8")) except Exception: return e.code, {} def raw(method, path): r = urllib.request.Request(BASE_URL + path, method=method) with urllib.request.urlopen(r, timeout=5) as resp: return resp.status, resp.read().decode("utf-8") def main(): if os.path.exists(STATE): shutil.rmtree(STATE) os.makedirs(STATE, exist_ok=True) env = dict(os.environ, TRAINING_STATE_DIR=STATE, PORT=str(PORT)) proc = subprocess.Popen( [sys.executable, os.path.join(BASE, "server.py")], env=env, cwd=BASE, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) try: for _ in range(40): time.sleep(0.25) try: urllib.request.urlopen(BASE_URL + "/api/meta", timeout=1) break except Exception: pass else: print("Server nicht erreichbar") sys.exit(1) st, meta = req("GET", "/api/meta") check("meta", st == 200 and "today" in meta and "now" in meta, meta) st, cfg = req("GET", "/api/config") acts = cfg["activities"] check("config hat 3 Aktivitaeten", len(acts) == 3, [a["id"] for a in acts]) gym = next(a for a in acts if a["id"] == "gym") check("gym normalisiert (Push/Pull/Legs)", len(gym["cycle"]["days"]) == 3, [d["key"] for d in gym["cycle"]["days"]]) push = gym["cycle"]["days"][0] ex = push["exercises"][0] check("Uebung hat key + sets", ex["key"] == "Push/Bankdrücken KH (je Hand)" and ex["sets"]["count"] == 4, ex) run = next(a for a in acts if a["id"] == "run") check("run fieldTemplates", len(run["fieldTemplates"]) == 5, run["fieldTemplates"]) st, static = raw("GET", "/") check("index.html", st == 200 and " 0, stats) check("Statistik: bestes Gewicht je Termin", s0["weight"] == 22 and s1["weight"] == 20, stats) st, exp = req("GET", "/api/export?format=json") check("Export JSON Format", st == 200 and exp["format"] == "trainingsplan-export-v2" and len(exp["sessions"]) == 4, exp.get("format")) st, body = raw("GET", "/api/export?format=csv") check("Export CSV Header", st == 200 and "session_id" in body and "distanceKm" in body, body[:120]) st, imp = req("POST", "/api/import", {"format": "trainingsplan-export-v2", "data": json.dumps(exp)}) check("Import JSON (Merge) ok", st == 200 and imp["created"] == 0 and imp["updated"] == 4, imp) st, csvimp = req("POST", "/api/import", {"format": "csv", "data": body}) check("Import CSV ok", st == 200 and csvimp["updated"] == 4, csvimp) st, r = req("DELETE", f"/api/sessions/{cid}") check("Session loeschen", st == 200 and r["deleted"] == 1, r) st, r = req("DELETE", f"/api/sessions/{sid2}") check("Session loeschen (sid2)", r.get("deleted") == 1, r) st, r = req("POST", "/api/sessions", {}) check("POST ohne activity -> 400", st == 400, (st, r)) st, r = req("PATCH", "/api/sessions/99999", {"meta": {"note": "x"}}) check("PATCH unbekannte Session -> 404", st == 404, (st, r)) st, r = req("GET", "/api/nonexistent") check("unbekannter Pfad -> 404", st == 404) finally: proc.terminate() proc.wait(timeout=5) print(f"\n{PASS} ok, {FAIL} fail") sys.exit(1 if FAIL else 0) if __name__ == "__main__": main()