Initial commit
This commit is contained in:
493
doormile_test.py
Normal file
493
doormile_test.py
Normal file
@@ -0,0 +1,493 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user