Initial commit

This commit is contained in:
2026-06-26 16:08:31 +05:30
commit f49193ee73
23 changed files with 6056 additions and 0 deletions

493
doormile_test.py Normal file
View 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())