#!/usr/bin/env python3 """Round 11 API Contract Test - with rate-limit awareness""" import urllib.request import urllib.error import json import time import sys import os BASE = "http://localhost:8080/api/v1" PASS = 0 FAIL = 0 TOKEN = "" ADMIN_TOKEN = "" SESS_ID = "" def req(method, path, data=None, auth=None, ct="application/json"): url = f"{BASE}{path}" headers = {"Content-Type": ct} if auth: headers["Authorization"] = f"Bearer {auth}" body = None if data is not None: body = json.dumps(data).encode() try: r = urllib.request.Request(url, data=body, headers=headers, method=method) resp = urllib.request.urlopen(r, timeout=10) return resp.status, resp.read().decode() except urllib.error.HTTPError as e: return e.code, e.read().decode() except Exception as e: return 0, str(e) def test(name, method, path, expected, data=None, auth=None, skip_msg=None): global PASS, FAIL if skip_msg: print(f" SKIP | {name}: {skip_msg}") return None, None code, body = req(method, path, data, auth) status = "PASS" if code == expected else "FAIL" if status == "PASS": PASS += 1 else: FAIL += 1 body_preview = body[:100].replace('\n',' ') if body else "" print(f" {status} | {name} | expected={expected} got={code} | {body_preview}") return code, body print("=" * 60) print(" Round 11 API Contract Test Suite") print(f" Started at {time.strftime('%Y-%m-%d %H:%M:%S')}") print("=" * 60) # === Part 1: Health === print("\n--- Part 1: Health ---") test("GET /health", "GET", "/health", 200) test("HEAD /health", "HEAD", "/health", 200) # === Part 2: Auth Register === print("\n--- Part 2: Auth Register ---") UN = f"testapi_{int(time.time())}" PW = "TestPass123!" test("Register missing username", "POST", "/auth/register", 400, {"password":"Test123!","email":"test@test.com","nickname":"Test","verify_code":"000000"}) time.sleep(1) test("Register missing password", "POST", "/auth/register", 400, {"username":"testuser","email":"test@test.com","nickname":"Test","verify_code":"000000"}) time.sleep(1) test("Register username too short", "POST", "/auth/register", 400, {"username":"ab","password":"Test123!","email":"test@test.com","nickname":"Test","verify_code":"000000"}) time.sleep(1) test("Register username too long", "POST", "/auth/register", 400, {"username":"a"*33,"password":"Test123!","email":"test@test.com","nickname":"Test","verify_code":"000000"}) time.sleep(1) test("Register username special chars", "POST", "/auth/register", 400, {"username":"user@name!","password":"Test123!","email":"test@test.com","nickname":"Test","verify_code":"000000"}) time.sleep(1) test("Register password too short", "POST", "/auth/register", 400, {"username":"test_abc","password":"Ab1!","email":"test@test.com","nickname":"Test","verify_code":"000000"}) time.sleep(1) # Normal registration code, body = test("Register normal", "POST", "/auth/register", 201, {"username":UN,"password":PW,"email":"test@test.com","nickname":"TestUser","verify_code":"000000"}) time.sleep(1) # Duplicate test("Register duplicate", "POST", "/auth/register", 409, {"username":UN,"password":PW,"email":"test@test.com","nickname":"TestUser","verify_code":"000000"}) # === Part 3: Auth Login === print("\n--- Part 3: Auth Login ---") time.sleep(1) test("Login wrong username", "POST", "/auth/login", 401, {"username":"nonexistent_user_99","password":"TestPass123!"}) time.sleep(1) test("Login wrong password", "POST", "/auth/login", 401, {"username":UN,"password":"WrongPass999!"}) time.sleep(1) # Correct login code, body = req("POST", "/auth/login", {"username":UN,"password":PW}) if code == 200: data = json.loads(body) TOKEN = data.get("token","") print(f" PASS | Login correct | 200 | user_id={data.get('user_id')} token={'OK' if TOKEN else 'MISSING'}") PASS += 1 else: print(f" FAIL | Login correct | expected=200 got={code} | {body[:100]}") FAIL += 1 time.sleep(1) # Admin login code, body = req("POST", "/auth/login", {"username":"admin","password":"admin123"}) if code == 200: data = json.loads(body) ADMIN_TOKEN = data.get("token","") print(f" PASS | Admin login | 200 | token={'OK' if ADMIN_TOKEN else 'MISSING'}") PASS += 1 else: print(f" FAIL | Admin login | expected=200 got={code} | {body[:100]}") FAIL += 1 # JWT validation if TOKEN: time.sleep(1) test("JWT valid (refresh)", "POST", "/auth/refresh", 200, auth=TOKEN) time.sleep(1) test("Login missing password", "POST", "/auth/login", 400, {"username":"testuser"}) time.sleep(1) test("Login empty body", "POST", "/auth/login", 400, {}) time.sleep(1) test("Login username format invalid", "POST", "/auth/login", 400, {"username":"ab","password":"Test123!"}) # === Part 4: Session API === print("\n--- Part 4: Session API ---") test("Sessions list no auth", "GET", "/sessions", 401) test("Sessions create no auth", "POST", "/sessions", 401, {"title":"Test"}) if TOKEN: time.sleep(1) test("Sessions list with auth", "GET", "/sessions", 200, auth=TOKEN) time.sleep(1) code, body = test("Sessions create", "POST", "/sessions", 201, {"title":"Round 11 Test"}, auth=TOKEN) if code == 201: try: SESS_ID = json.loads(body).get("id","") except: pass if SESS_ID: time.sleep(1) test("Sessions get existing", "GET", f"/sessions/{SESS_ID}", 200, auth=TOKEN) time.sleep(1) test("Sessions get non-existent", "GET", "/sessions/session_nonexistent123", 404, auth=TOKEN) time.sleep(1) test("Sessions create empty title", "POST", "/sessions", 201, {}, auth=TOKEN) if SESS_ID: time.sleep(1) test("Sessions delete existing", "DELETE", f"/sessions/{SESS_ID}", 200, auth=TOKEN) time.sleep(1) test("Sessions delete non-existent", "DELETE", "/sessions/session_nonexistent", 200, auth=TOKEN) time.sleep(1) test("Messages get non-existent session", "GET", "/sessions/session_nonexistent/messages", 200, auth=TOKEN) else: print(" SKIP: No token available for session tests") # === Part 5: Files === print("\n--- Part 5: Files API ---") test("Files list no auth", "GET", "/files", 401) if TOKEN: time.sleep(1) test("Files list with auth", "GET", "/files", 200, auth=TOKEN) time.sleep(1) test("Files get non-existent", "GET", "/files/file_nonexistent", 404, auth=TOKEN) # === Part 6: Knowledge === print("\n--- Part 6: Knowledge API ---") test("Knowledge bases no auth", "GET", "/knowledge/bases", 401) if TOKEN: time.sleep(1) test("Knowledge bases with auth", "GET", "/knowledge/bases", 200, auth=TOKEN) time.sleep(1) test("Knowledge get non-existent", "GET", "/knowledge/bases/kb_nonexistent", 404, auth=TOKEN) # === Part 7: Automation === print("\n--- Part 7: Automation API ---") test("Automation rules no auth", "GET", "/automation/rules", 401) if TOKEN: time.sleep(1) test("Automation rules with auth", "GET", "/automation/rules", 200, auth=TOKEN) time.sleep(1) test("Automation scenes with auth", "GET", "/automation/scenes", 200, auth=TOKEN) time.sleep(1) test("Automation get non-existent rule", "GET", "/automation/rules/rule_nonexistent", 404, auth=TOKEN) # === Part 8: Reminders === print("\n--- Part 8: Reminders API ---") test("Reminders list no auth", "GET", "/reminders", 401) if TOKEN: time.sleep(1) test("Reminders list with auth (needs user_id)", "GET", "/reminders?user_id=test_user", 200, auth=TOKEN) time.sleep(1) test("Reminders create missing fields", "POST", "/reminders", 400, {}, auth=TOKEN) # === Part 9: Briefings === print("\n--- Part 9: Briefings API ---") test("Briefings get no auth", "GET", "/briefings", 401) if TOKEN: time.sleep(1) test("Briefings get with auth", "GET", "/briefings?user_id=test_user", 200, auth=TOKEN) time.sleep(1) test("Briefings latest with auth", "GET", "/briefings/latest?user_id=test_user", 200, auth=TOKEN) # === Part 10: Notifications === print("\n--- Part 10: Notifications API ---") test("Notifications push no auth", "POST", "/notifications/push", 401, {"message":"test"}) if TOKEN: time.sleep(1) test("Notifications push empty", "POST", "/notifications/push", 400, {}, auth=TOKEN) # === Part 11: Memories === print("\n--- Part 11: Memories API ---") test("Memories search no auth", "GET", "/memory/search", 401) if TOKEN: time.sleep(1) test("Memories search with auth", "GET", "/memory/search?q=test", 200, auth=TOKEN) time.sleep(1) test("Memories list with auth", "GET", "/memory", 200, auth=TOKEN) time.sleep(1) test("Memories add missing fields", "POST", "/memory", 400, {}, auth=TOKEN) # === Part 12: Voice === print("\n--- Part 12: Voice API ---") if TOKEN: time.sleep(1) test("Voice status with auth", "GET", "/voice/status", 200, auth=TOKEN) # === Part 13: Admin === print("\n--- Part 13: Admin endpoints ---") if ADMIN_TOKEN: time.sleep(1) test("Admin sessions with admin", "GET", "/admin/sessions", 200, auth=ADMIN_TOKEN) time.sleep(1) test("Admin sessions/active", "GET", "/admin/sessions/active", 200, auth=ADMIN_TOKEN) if TOKEN: time.sleep(1) test("Admin sessions without admin", "GET", "/admin/sessions", 403, auth=TOKEN) # === Part 14: Invalid token === print("\n--- Part 14: Invalid token ---") test("Sessions invalid token", "GET", "/sessions", 401, auth="invalidtoken12345") test("Sessions expired/malformed token", "GET", "/sessions", 401, auth="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8k") # === Part 15: Token refresh === print("\n--- Part 15: Token refresh ---") test("Refresh no auth", "POST", "/auth/refresh", 401) test("Refresh invalid token", "POST", "/auth/refresh", 401, auth="invalidtoken") # === Summary === print("\n" + "=" * 60) print(" TEST SUMMARY") print("=" * 60) TOTAL = PASS + FAIL print(f"Total: {TOTAL} | Passed: {PASS} | Failed: {FAIL}") print(f"Success rate: {PASS/TOTAL*100:.1f}%" if TOTAL > 0 else "No tests run") print(f"\nTest user: {UN}") print(f"Token: {'available' if TOKEN else 'MISSING'}") print(f"Admin token: {'available' if ADMIN_TOKEN else 'MISSING'}") # Save env if TOKEN: with open("/tmp/round11_testenv", "w") as f: f.write(f"TOKEN={TOKEN}\n") f.write(f"ADMIN_TOKEN={ADMIN_TOKEN}\n") f.write(f"TEST_USER={UN}\n")