Files
Bentlybro e82cda69e0 refactor: rewrite e2e tests for clarity and correctness
- Fix login test that fails because registration auto-authenticates
- Add modular test structure with clear pass/fail reporting
- Skip moderation tests in CI (detects test-key, no real OpenAI)
- Actually run all test functions (some were defined but never called)
- Early exit on critical failures instead of running useless tests
- Clean up CI workflow env vars
2026-02-17 10:28:25 +00:00

411 lines
13 KiB
Python

#!/usr/bin/env python3
"""
E2E Tests for AutoModerate
Tests the complete content moderation workflow:
- Platform health & auth
- Project and API key management
- Content moderation (safe + suspicious content)
- Stats endpoint
Run with: python tests/e2e_test.py
Or: pytest tests/e2e_test.py -v
"""
import os
import random
import re
import string
import sys
import time
import requests
from bs4 import BeautifulSoup
class AutoModerateClient:
"""HTTP client for AutoModerate with session management."""
def __init__(self, base_url: str = "http://localhost:6217"):
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
self.session.headers.update({"User-Agent": "AutoModerate-E2E-Test/2.0"})
def get(self, path: str, **kwargs) -> requests.Response:
return self.session.get(f"{self.base_url}{path}", timeout=30, **kwargs)
def post(self, path: str, **kwargs) -> requests.Response:
return self.session.post(f"{self.base_url}{path}", timeout=30, **kwargs)
def get_csrf_token(self, path: str) -> str | None:
"""Extract CSRF token from a form page."""
resp = self.get(path, allow_redirects=False)
if resp.status_code != 200:
return None
soup = BeautifulSoup(resp.text, "html.parser")
# Try input field first
csrf_input = soup.find("input", {"name": "csrf_token"})
if csrf_input:
return csrf_input.get("value")
# Try meta tag
csrf_meta = soup.find("meta", {"name": "csrf-token"})
if csrf_meta:
return csrf_meta.get("content")
return None
def api_get(self, path: str, api_key: str, **kwargs) -> requests.Response:
headers = kwargs.pop("headers", {})
headers["X-API-Key"] = api_key
return self.get(path, headers=headers, **kwargs)
def api_post(self, path: str, api_key: str, **kwargs) -> requests.Response:
headers = kwargs.pop("headers", {})
headers["X-API-Key"] = api_key
headers["Content-Type"] = "application/json"
return self.post(path, headers=headers, **kwargs)
class TestResult:
"""Simple test result tracker."""
def __init__(self):
self.passed = 0
self.failed = 0
self.results = []
def ok(self, name: str):
self.passed += 1
self.results.append((name, True, None))
print(f" ✓ {name}")
def fail(self, name: str, reason: str = ""):
self.failed += 1
self.results.append((name, False, reason))
print(f" ✗ {name}" + (f" — {reason}" if reason else ""))
@property
def success(self) -> bool:
return self.failed == 0
def summary(self) -> str:
total = self.passed + self.failed
return f"{self.passed}/{total} tests passed"
def random_suffix(length: int = 8) -> str:
return "".join(random.choices(string.ascii_lowercase, k=length))
# =============================================================================
# Test Functions
# =============================================================================
def test_health(client: AutoModerateClient, results: TestResult):
"""Test that the application is running and healthy."""
try:
resp = client.get("/api/health")
if resp.status_code == 200:
results.ok("health check")
else:
results.fail("health check", f"status {resp.status_code}")
except Exception as e:
results.fail("health check", str(e))
def test_register_and_auth(client: AutoModerateClient, results: TestResult) -> dict | None:
"""Register a new user and verify authentication works."""
suffix = random_suffix()
user = {
"username": f"testuser_{suffix}",
"email": f"test_{suffix}@example.com",
"password": f"TestPass123_{suffix}",
}
try:
# Get CSRF token
csrf = client.get_csrf_token("/auth/register")
if not csrf:
results.fail("register", "couldn't get CSRF token")
return None
# Register
resp = client.post(
"/auth/register",
data={**user, "csrf_token": csrf},
allow_redirects=False,
)
# Should redirect to dashboard (registration auto-logs in)
if resp.status_code == 302 and "/dashboard" in resp.headers.get("Location", ""):
results.ok("register + auto-login")
return user
else:
results.fail("register", f"unexpected response: {resp.status_code}")
return None
except Exception as e:
results.fail("register", str(e))
return None
def test_create_project(client: AutoModerateClient, results: TestResult) -> str | None:
"""Create a new project and return its ID."""
suffix = random_suffix()
try:
csrf = client.get_csrf_token("/dashboard/projects/create")
if not csrf:
results.fail("create project", "couldn't get CSRF token (not logged in?)")
return None
resp = client.post(
"/dashboard/projects/create",
data={
"csrf_token": csrf,
"name": f"Test Project {suffix}",
"description": f"E2E test project {suffix}",
},
allow_redirects=False,
)
if resp.status_code not in (301, 302):
results.fail("create project", f"unexpected status: {resp.status_code}")
return None
location = resp.headers.get("Location", "")
# Extract project UUID from redirect
uuid_match = re.search(
r"/dashboard/projects/([0-9a-fA-F-]{36})", location
)
if uuid_match:
project_id = uuid_match.group(1)
results.ok("create project")
return project_id
# Sometimes redirects to project list — fetch and find it
if "/dashboard/projects" in location:
list_resp = client.get("/dashboard/projects")
uuid_match = re.search(
r"/dashboard/projects/([0-9a-fA-F-]{36})", list_resp.text
)
if uuid_match:
project_id = uuid_match.group(1)
results.ok("create project")
return project_id
results.fail("create project", "couldn't extract project ID")
return None
except Exception as e:
results.fail("create project", str(e))
return None
def test_get_api_key(client: AutoModerateClient, results: TestResult, project_id: str) -> str | None:
"""Get or create an API key for the project."""
try:
resp = client.get(f"/dashboard/projects/{project_id}/api-keys")
if resp.status_code != 200:
results.fail("get API key", f"couldn't access API keys page: {resp.status_code}")
return None
# Look for existing API key
key_match = re.search(r"(am_[a-zA-Z0-9_-]+)", resp.text)
if key_match:
results.ok("get API key")
return key_match.group(1)
# Create one if none exists
csrf = client.get_csrf_token(f"/dashboard/projects/{project_id}/api-keys")
if not csrf:
results.fail("get API key", "no existing key and couldn't get CSRF to create one")
return None
create_resp = client.post(
f"/dashboard/projects/{project_id}/api-keys/create",
data={"csrf_token": csrf, "name": f"E2E Test Key {random_suffix()}"},
allow_redirects=True,
)
key_match = re.search(r"(am_[a-zA-Z0-9_-]+)", create_resp.text)
if key_match:
results.ok("get API key (created)")
return key_match.group(1)
results.fail("get API key", "couldn't create API key")
return None
except Exception as e:
results.fail("get API key", str(e))
return None
def test_moderate_safe_content(client: AutoModerateClient, results: TestResult, api_key: str):
"""Test that safe content is approved."""
try:
resp = client.api_post(
"/api/moderate",
api_key,
json={
"type": "text",
"content": "Thank you for your excellent customer service! The documentation is clear and helpful.",
"metadata": {"source": "e2e_test", "test_type": "safe"},
},
)
if resp.status_code != 200:
results.fail("moderate safe content", f"status {resp.status_code}")
return
data = resp.json()
if data.get("success") and data.get("status") == "approved":
results.ok("moderate safe content → approved")
else:
results.fail("moderate safe content", f"expected approved, got: {data}")
except Exception as e:
results.fail("moderate safe content", str(e))
def test_moderate_suspicious_content(client: AutoModerateClient, results: TestResult, api_key: str):
"""Test that suspicious/harmful content is flagged or rejected."""
try:
resp = client.api_post(
"/api/moderate",
api_key,
json={
"type": "text",
"content": (
"🚨 URGENT: Your account will be SUSPENDED! "
"Click here immediately: fake-bank.com/verify "
"Enter your SSN, credit card, and password NOW or lose access forever!"
),
"metadata": {"source": "e2e_test", "test_type": "suspicious"},
},
)
if resp.status_code != 200:
results.fail("moderate suspicious content", f"status {resp.status_code}")
return
data = resp.json()
if data.get("success") and data.get("status") in ("rejected", "flagged"):
results.ok(f"moderate suspicious content → {data.get('status')}")
else:
results.fail("moderate suspicious content", f"expected rejected/flagged, got: {data}")
except Exception as e:
results.fail("moderate suspicious content", str(e))
def test_api_stats(client: AutoModerateClient, results: TestResult, api_key: str):
"""Test the stats endpoint returns valid data."""
try:
resp = client.api_get("/api/stats", api_key)
if resp.status_code != 200:
results.fail("API stats", f"status {resp.status_code}")
return
data = resp.json()
if data.get("success"):
results.ok("API stats")
else:
results.fail("API stats", f"success=false: {data}")
except Exception as e:
results.fail("API stats", str(e))
# =============================================================================
# Main Runner
# =============================================================================
def run_tests(base_url: str = "http://localhost:6217", include_moderation: bool = True) -> bool:
"""
Run the E2E test suite.
Args:
base_url: AutoModerate server URL
include_moderation: Whether to run moderation tests (requires OpenAI key)
Returns:
True if all tests passed
"""
print(f"\n{'='*60}")
print("AutoModerate E2E Tests")
print(f"{'='*60}")
print(f"Target: {base_url}")
print()
client = AutoModerateClient(base_url)
results = TestResult()
# Phase 1: Platform Core
print("▸ Platform Core")
test_health(client, results)
user = test_register_and_auth(client, results)
if not user:
print(f"\n{results.summary()}")
print("⚠ Stopping early — auth failed\n")
return False
project_id = test_create_project(client, results)
if not project_id:
print(f"\n{results.summary()}")
print("⚠ Stopping early — project creation failed\n")
return False
api_key = test_get_api_key(client, results, project_id)
if not api_key:
print(f"\n{results.summary()}")
print("⚠ Stopping early — API key retrieval failed\n")
return False
# Phase 2: Moderation API
if include_moderation:
print("\n▸ Moderation API")
test_moderate_safe_content(client, results, api_key)
test_moderate_suspicious_content(client, results, api_key)
test_api_stats(client, results, api_key)
else:
print("\n▸ Moderation API (skipped — no OpenAI key)")
# Summary
print(f"\n{'='*60}")
print(results.summary())
if results.success:
print("✅ All tests passed!")
else:
print("❌ Some tests failed")
print(f"{'='*60}\n")
return results.success
def main():
base_url = os.getenv("BASE_URL", "http://localhost:6217")
# Check if we should run moderation tests
# In CI with a test key, moderation will fail — skip those tests
openai_key = os.getenv("OPENAI_API_KEY", "")
include_moderation = openai_key and not openai_key.startswith("test")
# Give the app a moment to be fully ready
time.sleep(1)
success = run_tests(base_url, include_moderation=include_moderation)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()