Files
tianxuan/tests/test_integration.py
T

133 lines
4.5 KiB
Python

"""
Full-stack integration test. Run: python tests/test_integration.py
IMPORTANT: Update this file whenever frontend HTML/API changes.
Pure Python — no curl, no requests required.
"""
import subprocess, time, json, sys, threading, os, uuid, urllib.request, urllib.error
PORT = 18765
BASE = f"http://127.0.0.1:{PORT}"
errors = []
_lock = threading.Lock()
def fail(msg):
with _lock: errors.append(msg)
def check(cond, msg):
if not cond: fail(msg)
def start_server():
manage_py = os.path.join(os.path.dirname(__file__), '..', 'manage.py')
proc = subprocess.Popen(
[sys.executable, manage_py, 'runserver', f'127.0.0.1:{PORT}', '--noreload'],
stderr=subprocess.PIPE, stdout=subprocess.PIPE, text=True, encoding='utf-8',
)
def monitor():
for line in proc.stderr:
stripped = line.strip()
if 'ERROR' in stripped.upper() and 'GET' not in stripped and 'POST' not in stripped:
fail(f"STDERR: {stripped}")
threading.Thread(target=monitor, daemon=True).start()
return proc
def build_multipart_formdata(fields):
"""Build multipart/form-data body and content-type boundary.
fields: {name: (filename, content_type, data_bytes)}
"""
boundary = uuid.uuid4().hex
lines = []
for name, (filename, content_type, data) in fields.items():
lines.append(f'--{boundary}'.encode())
lines.append(f'Content-Disposition: form-data; name="{name}"; filename="{filename}"'.encode())
lines.append(f'Content-Type: {content_type}'.encode())
lines.append(b'')
lines.append(data)
lines.append(f'--{boundary}--'.encode())
return b'\r\n'.join(lines), f'multipart/form-data; boundary={boundary}'
def http_get(path, max_retries=10):
"""GET request with retry on connection refused."""
for attempt in range(max_retries):
try:
r = urllib.request.urlopen(f'{BASE}{path}', timeout=5)
body = r.read()
code = r.status
r.close()
return code, body
except urllib.error.URLError as e:
if 'refused' in str(e).lower() and attempt < max_retries - 1:
time.sleep(1)
continue
return 502, b'Connection refused'
def http_post_multipart(path, fields):
"""POST multipart form data, returns (status_code, body_bytes)."""
body_bytes, content_type = build_multipart_formdata(fields)
req = urllib.request.Request(f'{BASE}{path}', data=body_bytes,
headers={'Content-Type': content_type})
try:
r = urllib.request.urlopen(req, timeout=30)
return r.status, r.read()
except urllib.error.HTTPError as e:
return e.code, e.read()
def main():
proc = start_server()
time.sleep(3)
# Test 1: Homepage
code, _ = http_get('/')
check(code == 200, f"GET / → {code}")
# Test 2: Upload CSV with '+' in lat/lon
csv_content = (
":ips,:ipd,0ver,:prd,:prs,:ips.latd,:ips.lond\n"
"1.2.3.4,5.6.7.8,0303,443,80,35.0,139.0\n"
"9.10.11.12,13.14.15.16,0304,8080,443,+,+\n"
)
fields = {'files': ('test.csv', 'text/csv', csv_content.encode('utf-8'))}
code, body = http_post_multipart('/upload/csv/', fields)
check(code == 200, f"POST /upload/csv/ → {code}")
data = json.loads(body)
run_id = data.get('run_id')
check(run_id is not None, "No run_id in upload response")
# Test 3: Poll run status until ready
for _ in range(30):
code, body = http_get(f'/runs/{run_id}/status/')
check(code == 200, f"GET /runs/{run_id}/status/ → {code}")
s = json.loads(body)
if s['status'] in ('ready', 'completed', 'failed'):
check(s['status'] != 'failed', f"Run failed: {s.get('error_message','')}")
break
time.sleep(1)
# Test 4: Run list page
code, _ = http_get('/runs/')
check(code == 200, f"GET /runs/ → {code}")
# Test 5: Globe page
code, _ = http_get('/globe/')
check(code == 200, f"GET /globe/ → {code}")
# Test 6: Config page
code, _ = http_get('/config/')
check(code == 200, f"GET /config/ → {code}")
# Test 7: Delete run — @csrf_exempt means 200 (or 500), not 403, not 404
code, _ = http_post_multipart(f'/runs/{run_id}/delete/', {})
check(code != 404, f"POST /runs/{run_id}/delete/ → {code} (should not be 404)")
# Cleanup
proc.terminate()
proc.wait()
if errors:
print("INTEGRATION TEST FAILED:")
for e in errors: print(f" {e}")
sys.exit(1)
print("ALL INTEGRATION TESTS PASSED")
if __name__ == "__main__":
main()