Files
AI_engine/doormile_test.py
2026-06-26 16:08:31 +05:30

493 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Doormile Full System Test Suite v2
Fixed: correct routes, 2-step login, pricing payload, health endpoint
"""
import asyncio
import aiohttp
import json
import time
import websockets
import nats
import redis.asyncio as aioredis
from datetime import datetime
# ── Config ────────────────────────────────────────────────────────────────────
API_BASE = "https://api.doormile.com"
NATS_URL = "nats://66.116.226.161:4223"
NATS_USER = "doormile"
NATS_PASSWORD = "Package@321#"
REDIS_HOST = "66.116.226.255"
REDIS_PORT = 6380
REDIS_PASSWORD = "Package@321#"
INTERNAL_KEY = "doormile-internal-2024"
# Test customer — uses the one created by Window 1 test
TEST_PHONE = "9900000001"
TEST_PIN = "9999"
# Colors
GREEN = "\033[92m"
RED = "\033[91m"
YELLOW = "\033[93m"
BLUE = "\033[94m"
CYAN = "\033[96m"
BOLD = "\033[1m"
RESET = "\033[0m"
results = []
def log(msg, color=RESET):
ts = datetime.now().strftime("%H:%M:%S")
print(f"{color}[{ts}] {msg}{RESET}")
def pass_test(name, detail=""):
results.append(("PASS", name))
log(f"✅ PASS — {name} {detail}", GREEN)
def fail_test(name, detail=""):
results.append(("FAIL", name))
log(f"❌ FAIL — {name} {detail}", RED)
def info(msg):
log(f" {msg}", CYAN)
def section(title):
print(f"\n{BOLD}{YELLOW}{'='*60}{RESET}")
print(f"{BOLD}{YELLOW} {title}{RESET}")
print(f"{BOLD}{YELLOW}{'='*60}{RESET}\n")
# ── Tests ─────────────────────────────────────────────────────────────────────
async def test_health(session):
section("1. HEALTH CHECK")
for path in ["/api/v1/health", "/api/v1/ready"]:
try:
async with session.get(f"{API_BASE}{path}",
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test(f"GET {path}", str(data))
else:
fail_test(f"GET {path}", f"status={r.status}")
except Exception as e:
fail_test(f"GET {path}", str(e))
async def login(session):
section("2. CUSTOMER LOGIN (2-step)")
token = None
try:
# Step 1
async with session.post(f"{API_BASE}/api/v1/customer/login",
json={"phone": TEST_PHONE},
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("Login step 1 — phone", f"phone={TEST_PHONE}")
info(str(data))
else:
fail_test("Login step 1 — phone", f"status={r.status} {data}")
return None
# Step 2
async with session.post(f"{API_BASE}/api/v1/customer/verify-pin",
json={"phone": TEST_PHONE, "pin": TEST_PIN},
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
token = data.get("token") or data.get("jwt") or data.get("accesstoken")
if token:
pass_test("Login step 2 — PIN verify", "JWT token obtained")
info(f"Token: {token[:50]}...")
else:
fail_test("Login step 2 — PIN verify", f"No token in resp: {data}")
else:
fail_test("Login step 2 — PIN verify", f"status={r.status} {data}")
except Exception as e:
fail_test("Login", str(e))
return token
async def test_profile(session, token):
section("3. CUSTOMER PROFILE")
try:
headers = {"Authorization": f"Bearer {token}"}
async with session.get(f"{API_BASE}/api/v1/customer/profile",
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("GET /customer/profile", f"name={data.get('firstname','')} {data.get('lastname','')}")
else:
fail_test("GET /customer/profile", f"status={r.status} {data}")
except Exception as e:
fail_test("Customer profile", str(e))
async def test_pricing(session, token):
section("4. PRICING CHECK (Redis-backed DoormilePricing)")
try:
# Correct payload — zone directly, not pincodes
payload = {
"zone": "OtherState",
"service_type": "Normal",
"weight": 1.0,
"itemcategory": "Documents"
}
headers = {"Authorization": f"Bearer {token}"}
async with session.post(f"{API_BASE}/api/v1/pricing/check",
json=payload,
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("Pricing check — OtherState/Normal", f"resp={data}")
else:
fail_test("Pricing check", f"status={r.status} resp={data}")
# Also test Local pricing
payload2 = {"zone": "Local", "service_type": "Normal",
"weight": 0.5, "itemcategory": "Documents"}
async with session.post(f"{API_BASE}/api/v1/pricing/check",
json=payload2,
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("Pricing check — Local/Normal", f"resp={data}")
else:
fail_test("Pricing check — Local/Normal", f"status={r.status} {data}")
except Exception as e:
fail_test("Pricing check", str(e))
async def test_city_gate(session, token):
section("5. CITY GATING — should REJECT Mumbai (400xxx)")
try:
payload = {
"pickupaddress": "MG Road, Mumbai",
"pickuppincode": "400001",
"pickuplatitude": 18.9388,
"pickuplongitude": 72.8354,
"deliveryaddress": "Hitech City, Hyderabad",
"deliverypincode": "500032",
"serviceoption": "Normal",
"parcels": [{"itemcategory": "Documents",
"itemdescription": "test", "weight": 0.5}]
}
headers = {"Authorization": f"Bearer {token}"}
async with session.post(f"{API_BASE}/api/v1/customer/bookings",
json=payload, headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 400:
pass_test("City gate — Mumbai rejected ✓", str(data))
else:
fail_test("City gate — Mumbai should be rejected",
f"got status={r.status} — city gate may not be active")
except Exception as e:
fail_test("City gate test", str(e))
async def create_booking(session, token):
section("6. CREATE BOOKING — Coimbatore → Hyderabad")
booking_id = None
try:
payload = {
"pickupaddress": "Gandhipuram, Coimbatore, Tamil Nadu",
"pickuppincode": "641001",
"pickuplatitude": 11.0168,
"pickuplongitude": 76.9558,
"deliveryaddress": "Hitech City, Hyderabad, Telangana",
"deliverypincode": "500032",
"serviceoption": "Normal",
"parcels": [
{
"itemcategory": "Documents",
"itemdescription": "Doormile E2E test parcel",
"weight": 0.5,
"length": 20,
"width": 15,
"height": 5
}
]
}
headers = {"Authorization": f"Bearer {token}"}
async with session.post(f"{API_BASE}/api/v1/customer/bookings",
json=payload, headers=headers,
timeout=aiohttp.ClientTimeout(total=15)) as r:
data = await r.json()
if r.status in [200, 201]:
# Try common field names for booking id
booking_id = (data.get("bookingid") or
data.get("booking_id") or
data.get("id") or
data.get("data", {}).get("bookingid"))
pass_test("Booking created", f"id={booking_id}")
info(f"Response: {json.dumps(data, indent=2)}")
else:
fail_test("Create booking", f"status={r.status} resp={data}")
except Exception as e:
fail_test("Create booking", str(e))
return booking_id
async def test_assignment(session, token, booking_id):
section("7. AUTO-ASSIGNMENT ENGINE (waiting 12s...)")
if not booking_id:
fail_test("Assignment check", "No booking_id from step 6")
return
info("Giving assignment engine 12 seconds to fire goroutine...")
for i in range(12, 0, -3):
info(f" {i}s remaining...")
await asyncio.sleep(3)
try:
headers = {"Authorization": f"Bearer {token}"}
async with session.get(f"{API_BASE}/api/v1/customer/bookings/{booking_id}",
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
status = (data.get("status") or
data.get("booking", {}).get("status", "unknown"))
miler = (data.get("assignedmileruserid") or
data.get("booking", {}).get("assignedmileruserid", "none"))
info(f"Booking status: {status}")
info(f"Assigned miler: {miler}")
if status in ["Miler_Assigned", "Pickup_Scheduled"]:
pass_test("Auto-assignment fired ✓", f"status={status} miler={miler}")
elif status == "Created":
fail_test("Auto-assignment", "Still 'Created' — check kubectl logs for assignment engine errors")
else:
pass_test("Booking updated", f"status={status}")
else:
fail_test("Check booking", f"status={r.status} {data}")
except Exception as e:
fail_test("Assignment check", str(e))
async def test_redis(booking_id):
section("8. REDIS — GEO index, booking cache, pricing cache")
try:
r = aioredis.Redis(host=REDIS_HOST, port=REDIS_PORT,
password=REDIS_PASSWORD, decode_responses=True)
await r.ping()
pass_test("Redis connection", f"{REDIS_HOST}:{REDIS_PORT}")
# Miler GEO index
count = await r.zcard("milers:locations")
if count > 0:
pass_test("milers:locations GEO index", f"{count} milers indexed")
# Find milers near Coimbatore
nearby = await r.georadius(
"milers:locations", 76.9558, 11.0168, 10, "km",
withcoord=True, count=5, sort="ASC"
)
if nearby:
pass_test("GEORADIUS near Coimbatore", f"found {len(nearby)} milers")
for m in nearby:
info(f" Miler: {m}")
else:
fail_test("GEORADIUS near Coimbatore", "No milers found in 10km radius")
else:
fail_test("milers:locations GEO index", "Empty — milers have no GPS in Redis")
# Booking cache
if booking_id:
data = await r.hgetall(f"bookings:{booking_id}")
if data:
pass_test("Booking Redis cache", f"keys={list(data.keys())}")
else:
fail_test("Booking Redis cache",
f"bookings:{booking_id} not in Redis — NATS worker may not be running")
# Pricing cache
keys = await r.keys("doormile:pricing:*")
if keys:
pass_test("Pricing Redis cache", f"{len(keys)} slabs cached")
else:
fail_test("Pricing Redis cache", "No pricing keys in Redis")
await r.aclose()
except Exception as e:
fail_test("Redis tests", str(e))
async def test_nats(booking_id):
section("9. NATS JETSTREAM — streams + publish test")
try:
nc = await nats.connect(
servers=[NATS_URL],
user=NATS_USER,
password=NATS_PASSWORD
)
js = nc.jetstream()
pass_test("NATS connected", NATS_URL)
streams = ["BOOKINGS", "TRACKING", "ASSIGNMENTS",
"NOTIFICATIONS", "CHAT", "STATUS"]
for stream in streams:
try:
info_obj = await js.stream_info(stream)
pass_test(f"Stream {stream}",
f"msgs={info_obj.state.messages}")
except Exception as e:
fail_test(f"Stream {stream}", str(e))
# Publish test GPS event
if booking_id:
evt = json.dumps({
"miler_id": "test_miler_001",
"booking_id": booking_id,
"lat": 11.0175,
"lon": 76.9565,
"timestamp": int(time.time())
})
ack = await js.publish("miler.location.updated", evt.encode())
pass_test("Publish miler.location.updated", f"seq={ack.seq}")
await nc.close()
except Exception as e:
fail_test("NATS tests", str(e))
async def test_internal_notify(session, booking_id):
section("10. INTERNAL NOTIFY API (/api/v1/internal/notify)")
if not booking_id:
fail_test("Internal notify", "No booking_id")
return
try:
payload = {
"booking_id": booking_id,
"target": "customer",
"title": "Doormile Test ✅",
"message": "E2E test notification — system is working!"
}
headers = {
"X-Internal-Key": INTERNAL_KEY,
"Content-Type": "application/json"
}
async with session.post(f"{API_BASE}/api/v1/internal/notify",
json=payload, headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("Internal notify", f"resp={data}")
else:
fail_test("Internal notify", f"status={r.status} resp={data}")
except Exception as e:
fail_test("Internal notify", str(e))
async def test_reassign(session, booking_id):
section("11. INTERNAL REASSIGN API")
if not booking_id:
fail_test("Internal reassign", "No booking_id")
return
try:
headers = {
"X-Internal-Key": INTERNAL_KEY,
"Content-Type": "application/json"
}
async with session.post(
f"{API_BASE}/api/v1/internal/bookings/{booking_id}/reassign",
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("Internal reassign", f"resp={data}")
elif r.status == 400:
info(f"Reassign returned 400 — booking may not be in assignable state: {data}")
pass_test("Internal reassign endpoint reachable", f"status=400 (expected if not assigned yet)")
else:
fail_test("Internal reassign", f"status={r.status} resp={data}")
except Exception as e:
fail_test("Internal reassign", str(e))
async def test_websocket(booking_id):
section("12. WEBSOCKET LIVE TRACKING")
if not booking_id:
fail_test("WebSocket", "No booking_id")
return
try:
ws_url = f"wss://api.doormile.com/ws/bookings/{booking_id}/track"
info(f"Connecting to {ws_url}")
async with websockets.connect(ws_url, open_timeout=10) as ws:
pass_test("WebSocket connected", ws_url)
try:
msg = await asyncio.wait_for(ws.recv(), timeout=5)
data = json.loads(msg)
pass_test("WebSocket frame received", str(data))
except asyncio.TimeoutError:
info("No frame in 5s — miler GPS not streaming yet (normal for test)")
pass_test("WebSocket connection works", "no GPS frame yet — expected")
except Exception as e:
fail_test("WebSocket", str(e))
async def test_booking_cache_api(session, token, booking_id):
section("13. BOOKING CACHE API (Redis GET endpoint)")
if not booking_id:
fail_test("Booking cache API", "No booking_id")
return
try:
headers = {"Authorization": f"Bearer {token}"}
async with session.get(
f"{API_BASE}/api/v1/bookings/cache/{booking_id}",
headers=headers,
timeout=aiohttp.ClientTimeout(total=10)) as r:
data = await r.json()
if r.status == 200:
pass_test("GET /bookings/cache/:id", f"fields={list(data.keys()) if isinstance(data,dict) else data}")
else:
fail_test("GET /bookings/cache/:id", f"status={r.status} {data}")
except Exception as e:
fail_test("Booking cache API", str(e))
def print_summary():
section("FINAL TEST SUMMARY")
passed = [r for r in results if r[0] == "PASS"]
failed = [r for r in results if r[0] == "FAIL"]
for status, name in results:
icon = "" if status == "PASS" else ""
color = GREEN if status == "PASS" else RED
print(f" {color}{icon} {name}{RESET}")
pct = int(len(passed) / len(results) * 100) if results else 0
print(f"\n{BOLD}{'='*60}{RESET}")
print(f"{BOLD} {len(results)} tests | "
f"{GREEN}{len(passed)} passed{RESET}{BOLD} | "
f"{RED}{len(failed)} failed{RESET}{BOLD} | "
f"{YELLOW}{pct}% success{RESET}")
print(f"{BOLD}{'='*60}{RESET}\n")
if failed:
print(f"{RED}Failed tests to investigate:{RESET}")
for _, name in failed:
print(f"{name}")
print()
async def main():
print(f"\n{BOLD}{BLUE}")
print("╔══════════════════════════════════════════════════════════╗")
print("║ DOORMILE FULL SYSTEM TEST SUITE v2 ║")
print("║ Backend · NATS · Redis · Assignment · WebSocket · FCM ║")
print("╚══════════════════════════════════════════════════════════╝")
print(f"{RESET}\n")
async with aiohttp.ClientSession() as session:
await test_health(session)
token = await login(session)
if not token:
fail_test("STOPPING — no auth token")
print_summary()
return
await test_profile(session, token)
await test_pricing(session, token)
await test_city_gate(session, token)
booking_id = await create_booking(session, token)
await test_assignment(session, token, booking_id)
await test_redis(booking_id)
await test_nats(booking_id)
await test_internal_notify(session, booking_id)
await test_reassign(session, booking_id)
await test_websocket(booking_id)
await test_booking_cache_api(session, token, booking_id)
print_summary()
if __name__ == "__main__":
asyncio.run(main())