feat(api): production security, cors hardening, latency optimizations, postman artifacts & frontend documentation
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import json
|
||||
import time
|
||||
|
||||
BASE_URL = 'http://127.0.0.1:8000'
|
||||
VALID_KEY = 'skyheal_secure_api_key_2026'
|
||||
|
||||
def http_post(url, body_dict=None, token=None, headers=None):
|
||||
if headers is None:
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
else:
|
||||
headers = dict(headers)
|
||||
if 'Content-Type' not in headers:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
if token:
|
||||
headers['Authorization'] = f"Bearer {token}"
|
||||
|
||||
data_bytes = json.dumps(body_dict).encode('utf-8') if body_dict is not None else b''
|
||||
req = urllib.request.Request(url, data=data_bytes, headers=headers, method='POST')
|
||||
try:
|
||||
with urllib.request.urlopen(req) as res:
|
||||
return res.status, json.loads(res.read().decode('utf-8')), res.headers
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode('utf-8', errors='ignore')
|
||||
try:
|
||||
parsed = json.loads(body)
|
||||
except:
|
||||
parsed = body
|
||||
return e.code, parsed, e.headers
|
||||
|
||||
def http_get(url, token=None, headers=None):
|
||||
if headers is None:
|
||||
headers = {}
|
||||
else:
|
||||
headers = dict(headers)
|
||||
if token:
|
||||
headers['Authorization'] = f"Bearer {token}"
|
||||
req = urllib.request.Request(url, headers=headers, method='GET')
|
||||
try:
|
||||
with urllib.request.urlopen(req) as res:
|
||||
ct = res.headers.get('Content-Type', '')
|
||||
body = res.read().decode('utf-8', errors='ignore')
|
||||
if 'json' in ct:
|
||||
return res.status, json.loads(body), res.headers
|
||||
return res.status, body, res.headers
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode('utf-8', errors='ignore')
|
||||
try:
|
||||
parsed = json.loads(body)
|
||||
except:
|
||||
parsed = body
|
||||
return e.code, parsed, e.headers
|
||||
|
||||
def run_automated_production_tests():
|
||||
test_results = []
|
||||
|
||||
# 1. Test Authentication Security
|
||||
c_no_auth, _, _ = http_post(f'{BASE_URL}/v1/onboarding/start', {"patient": {"id": "P1"}})
|
||||
test_results.append({"test": "1. Reject Unauthenticated Request (401)", "passed": c_no_auth == 401, "status": c_no_auth})
|
||||
|
||||
c_auth, r_auth, _ = http_post(f'{BASE_URL}/v1/onboarding/start', {
|
||||
"patient": {"id": "PAT-PROD-01", "name": "Prod Patient", "age": 32, "gender": "female"},
|
||||
"category": {"type": "general", "code": "general_medicine", "name": "General Medicine"}
|
||||
}, token="test_token_PAT-PROD-01")
|
||||
s_id = r_auth.get("session_id") if isinstance(r_auth, dict) else None
|
||||
test_results.append({"test": "2. Authenticated Session Creation (200)", "passed": c_auth == 200, "status": c_auth})
|
||||
|
||||
# 2. Test IDOR Session Isolation
|
||||
if s_id:
|
||||
c_idor, r_idor, _ = http_post(f'{BASE_URL}/v1/onboarding/{s_id}/message', {"message": "Unauthorized message"}, token="test_token_PAT-PROD-02")
|
||||
test_results.append({"test": "3. IDOR Prevention Cross-Session Access (403)", "passed": c_idor == 403, "status": c_idor})
|
||||
|
||||
# 3. Test Response Security Headers & X-Request-ID
|
||||
c_hdr, _, hdrs = http_get(f'{BASE_URL}/health')
|
||||
req_id = hdrs.get("X-Request-ID")
|
||||
nosniff = hdrs.get("X-Content-Type-Options")
|
||||
frame = hdrs.get("X-Frame-Options")
|
||||
hdr_passed = req_id is not None and nosniff == "nosniff" and frame == "DENY"
|
||||
test_results.append({"test": "4. Security Headers & X-Request-ID Correlation", "passed": hdr_passed, "status": c_hdr})
|
||||
|
||||
# 4. Test Age Validation Schema Bounds
|
||||
c_age_neg, _, _ = http_post(f'{BASE_URL}/v1/onboarding/start', {
|
||||
"patient": {"id": "PAT-NEG", "name": "Invalid Age", "age": -10, "gender": "female"},
|
||||
"category": {"type": "general", "code": "general_medicine", "name": "General Medicine"}
|
||||
}, token=VALID_KEY)
|
||||
test_results.append({"test": "5. Schema Validation Negative Age Rejection (422)", "passed": c_age_neg == 422, "status": c_age_neg})
|
||||
|
||||
total = len(test_results)
|
||||
passed = sum(1 for t in test_results if t['passed'])
|
||||
print(f"AUTOMATED PRODUCTION TESTS COMPLETED: {passed}/{total} Passed.")
|
||||
|
||||
with open('tests_production_results.json', 'w', encoding='utf-8') as f:
|
||||
json.dump({"total": total, "passed": passed, "results": test_results}, f, indent=2)
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_automated_production_tests()
|
||||
@@ -0,0 +1,125 @@
|
||||
import sys
|
||||
import os
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
def test_cors_allowed_origin_preflight():
|
||||
"""Test browser OPTIONS preflight for allowed origin (http://localhost:3000)."""
|
||||
response = client.options(
|
||||
"/v1/onboarding/start",
|
||||
headers={
|
||||
"Origin": "http://localhost:3000",
|
||||
"Access-Control-Request-Method": "POST",
|
||||
"Access-Control-Request-Headers": "authorization, content-type, x-api-key",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.headers.get("access-control-allow-origin") == "http://localhost:3000"
|
||||
assert response.headers.get("access-control-allow-credentials") == "true"
|
||||
assert "POST" in response.headers.get("access-control-allow-methods", "").upper()
|
||||
|
||||
def test_cors_allowed_origin_actual_request():
|
||||
"""Test actual POST request from allowed origin (http://localhost:5173)."""
|
||||
response = client.post(
|
||||
"/v1/onboarding/start",
|
||||
json={
|
||||
"patient": {"id": "PAT-CORS-1", "name": "CORS Test", "age": 30, "gender": "female"},
|
||||
"category": {"type": "general", "code": "general_medicine", "name": "General Medicine"},
|
||||
"language": "en"
|
||||
},
|
||||
headers={
|
||||
"Origin": "http://localhost:5173",
|
||||
"Authorization": "Bearer test_token_PAT-CORS-1"
|
||||
}
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.headers.get("access-control-allow-origin") == "http://localhost:5173"
|
||||
assert response.headers.get("access-control-allow-credentials") == "true"
|
||||
|
||||
def test_cors_unauthorized_request_returns_cors_headers():
|
||||
"""Test that 401 Unauthorized responses STILL return CORS headers to allowed origin."""
|
||||
response = client.post(
|
||||
"/v1/onboarding/start",
|
||||
json={"patient": {"id": "PAT-NOAUTH"}},
|
||||
headers={"Origin": "http://localhost:3000"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
assert response.headers.get("access-control-allow-origin") == "http://localhost:3000"
|
||||
assert response.headers.get("access-control-allow-credentials") == "true"
|
||||
|
||||
def test_cors_forbidden_request_returns_cors_headers():
|
||||
"""Test that 403 Forbidden responses STILL return CORS headers to allowed origin."""
|
||||
# 1. Create session for PAT-CORS-OWNER
|
||||
res_start = client.post(
|
||||
"/v1/onboarding/start",
|
||||
json={
|
||||
"patient": {"id": "PAT-CORS-OWNER", "name": "Owner", "age": 28, "gender": "male"},
|
||||
"category": {"type": "general", "code": "general_medicine", "name": "General Medicine"}
|
||||
},
|
||||
headers={"Authorization": "Bearer test_token_PAT-CORS-OWNER"}
|
||||
)
|
||||
session_id = res_start.json()["session_id"]
|
||||
|
||||
# 2. Access with different patient PAT-CORS-ATTACKER
|
||||
res_idor = client.post(
|
||||
f"/v1/onboarding/{session_id}/message",
|
||||
json={"message": "IDOR attempt"},
|
||||
headers={
|
||||
"Origin": "http://localhost:3000",
|
||||
"Authorization": "Bearer test_token_PAT-CORS-ATTACKER"
|
||||
}
|
||||
)
|
||||
assert res_idor.status_code == 403
|
||||
assert res_idor.headers.get("access-control-allow-origin") == "http://localhost:3000"
|
||||
assert res_idor.headers.get("access-control-allow-credentials") == "true"
|
||||
|
||||
def test_cors_disallowed_origin_rejected():
|
||||
"""Test that disallowed origin (https://malicious-example.com) is NOT allowed by CORS headers."""
|
||||
response = client.options(
|
||||
"/v1/onboarding/start",
|
||||
headers={
|
||||
"Origin": "https://malicious-example.com",
|
||||
"Access-Control-Request-Method": "POST",
|
||||
"Access-Control-Request-Headers": "authorization, content-type",
|
||||
},
|
||||
)
|
||||
# Browsers block preflight if access-control-allow-origin does not match
|
||||
assert response.headers.get("access-control-allow-origin") != "https://malicious-example.com"
|
||||
|
||||
def test_cors_voice_endpoints():
|
||||
"""Test preflight and actual POST on voice onboarding endpoints."""
|
||||
# Preflight voice start
|
||||
res_opt = client.options(
|
||||
"/v1/voice-onboarding/start",
|
||||
headers={
|
||||
"Origin": "http://localhost:5173",
|
||||
"Access-Control-Request-Method": "POST",
|
||||
"Access-Control-Request-Headers": "authorization, content-type"
|
||||
}
|
||||
)
|
||||
assert res_opt.status_code == 200
|
||||
assert res_opt.headers.get("access-control-allow-origin") == "http://localhost:5173"
|
||||
|
||||
# Actual voice start
|
||||
res_post = client.post(
|
||||
"/v1/voice-onboarding/start",
|
||||
json={
|
||||
"patient": {"id": "PAT-CORS-VOICE", "name": "Voice Test", "age": 40, "gender": "female"},
|
||||
"language": "en-IN"
|
||||
},
|
||||
headers={
|
||||
"Origin": "http://localhost:5173",
|
||||
"Authorization": "Bearer test_token_PAT-CORS-VOICE"
|
||||
}
|
||||
)
|
||||
assert res_post.status_code == 200
|
||||
assert res_post.headers.get("access-control-allow-origin") == "http://localhost:5173"
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main(["-v", __file__])
|
||||
Reference in New Issue
Block a user