#!/usr/bin/env python3 """Full API integration test for juwan-backend. Runs against the local dev docker-compose environment. Gateway: http://127.0.0.1:18080 Reads verification codes from local Redis only for dev registration flow. """ import json import random import string import sys import urllib.request import urllib.error import urllib.parse import http.cookiejar import os import subprocess GATEWAY = "http://127.0.0.1:18080" ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin") ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123") REDIS_CONTAINER = os.getenv("REDIS_CONTAINER", "juwan-redis") passed = 0 failed = 0 errors_list = [] def rand_str(n=8): return "".join(random.choices(string.ascii_lowercase + string.digits, k=n)) def as_int(value, default=0): try: return int(value) except (TypeError, ValueError): return default def pick_items(body): if isinstance(body.get("items"), list): return body["items"] if isinstance(body.get("list"), list): return body["list"] return [] def pick_user_id(body): if not isinstance(body, dict): return 0 if isinstance(body.get("user"), dict): return as_int(body["user"].get("id")) return as_int(body.get("id")) def read_vcode_from_redis(request_id, scene, account): if not request_id: return "" result = subprocess.run( [ "docker", "exec", REDIS_CONTAINER, "redis-cli", "GET", f"vcode:{request_id}:{scene}:{account}", ], capture_output=True, text=True, timeout=5, ) return result.stdout.strip() def build_multipart_form(fields, files): boundary = f"----juwan{rand_str(12)}" chunks = [] for name, value in fields.items(): chunks.extend( [ f"--{boundary}\r\n".encode(), f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode(), str(value).encode(), b"\r\n", ] ) for name, (filename, content, content_type) in files.items(): if isinstance(content, str): content = content.encode() chunks.extend( [ f"--{boundary}\r\n".encode(), ( f'Content-Disposition: form-data; name="{name}"; ' f'filename="{filename}"\r\n' ).encode(), f"Content-Type: {content_type}\r\n\r\n".encode(), content, b"\r\n", ] ) chunks.append(f"--{boundary}--\r\n".encode()) body = b"".join(chunks) return f"multipart/form-data; boundary={boundary}", body class Session: """Minimal cookie-aware HTTP session using stdlib only.""" def __init__(self): self.cookie_jar = http.cookiejar.CookieJar( policy=http.cookiejar.DefaultCookiePolicy( secure_protocols=("https", "http") ) ) self.opener = urllib.request.build_opener( urllib.request.HTTPCookieProcessor(self.cookie_jar) ) def get_cookie(self, name): for c in self.cookie_jar: if c.name == name: return c.value return None def request( self, method, url, json_body=None, headers=None, form_data=None, raw_body=None, ): hdrs = headers or {} body = None if json_body is not None: body = json.dumps(json_body).encode() hdrs.setdefault("Content-Type", "application/json") elif form_data is not None: body = urllib.parse.urlencode(form_data).encode() hdrs.setdefault("Content-Type", "application/x-www-form-urlencoded") elif raw_body is not None: body = raw_body req = urllib.request.Request(url, data=body, headers=hdrs, method=method) try: resp = self.opener.open(req, timeout=15) data = resp.read().decode() try: return resp.status, json.loads(data) if data else {}, dict(resp.headers) except json.JSONDecodeError: return resp.status, {"_raw": data}, dict(resp.headers) except urllib.error.HTTPError as e: data = e.read().decode() if e.fp else "" try: return e.code, json.loads(data) if data else {}, dict(e.headers) except json.JSONDecodeError: return e.code, {"_raw": data}, dict(e.headers) except Exception as e: return 0, {"_error": str(e)}, {} def get(self, url, **kw): return self.request("GET", url, **kw) def post(self, url, **kw): return self.request("POST", url, **kw) def post_multipart(self, url, fields, files, headers=None): hdrs = dict(headers or {}) content_type, body = build_multipart_form(fields, files) hdrs["Content-Type"] = content_type return self.post(url, headers=hdrs, raw_body=body) def put(self, url, **kw): return self.request("PUT", url, **kw) def delete(self, url, **kw): return self.request("DELETE", url, **kw) def csrf_headers(self): token = self.get_cookie("__Host-XSRF-TOKEN") return {"xsrf-token": token} if token else {} def report(name, status_code, body, expect_status=200): global passed, failed if isinstance(expect_status, (list, tuple, set)): ok = status_code in expect_status else: ok = status_code == expect_status mark = "PASS" if ok else "FAIL" if not ok: failed += 1 errors_list.append((name, status_code, body)) else: passed += 1 body_preview = json.dumps(body, ensure_ascii=False) if len(body_preview) > 200: body_preview = body_preview[:200] + "..." print(f" [{mark}] {name}: HTTP {status_code} {body_preview}") return ok def report_check(name, ok, body=None): global passed, failed mark = "PASS" if ok else "FAIL" if not ok: failed += 1 errors_list.append((name, "CHECK", body)) else: passed += 1 body_preview = json.dumps(body, ensure_ascii=False) if len(body_preview) > 200: body_preview = body_preview[:200] + "..." print(f" [{mark}] {name}: {body_preview}") return ok def skip(name, reason): print(f" [SKIP] {name}: {reason}") # ============================================================ # Phase 0: Health check & CSRF # ============================================================ def phase0_health(s: Session): print("\n=== Phase 0: Health & CSRF ===") code, body, _ = s.get(f"{GATEWAY}/healthz") report("GET /healthz", code, body) xsrf = s.get_cookie("__Host-XSRF-TOKEN") xsrf_guard = s.get_cookie("__Host-XSRF-GUARD") print(f" XSRF-TOKEN: {xsrf}") print(f" XSRF-GUARD: {xsrf_guard}") if not xsrf: print(" [WARN] No XSRF-TOKEN cookie received, POST requests will fail") # ============================================================ # Phase 1: Registration # ============================================================ def phase1_register(s: Session, username, email, password, label="user"): print(f"\n=== Phase 1: Register ({label}) ===") # Step 1: send verification code via gateway code, body, _ = s.post( f"{GATEWAY}/api/v1/email/verification-code/send", json_body={"email": email, "scene": "register"}, headers=s.csrf_headers(), ) report(f"POST /email/verification-code/send ({label})", code, body) request_id = body.get("requestId", "") if not request_id: print(" [ERROR] No requestId returned, cannot register") return None # Step 2: read the dev verification code from Redis print(" [BYPASS] Reading vcode from Redis...") vcode = read_vcode_from_redis(request_id, "register", email) if not vcode: print(" [WARN] No verification code found in Redis") else: print(f" Vcode from Redis: {vcode}") # Step 3: register via gateway with X-Request-Id csrf = s.csrf_headers() csrf["X-Request-Id"] = request_id code, body, _ = s.post( f"{GATEWAY}/api/v1/auth/register", json_body={ "username": username, "email": email, "password": password, "vcode": vcode, }, headers=csrf, ) report(f"POST /auth/register ({label})", code, body) return body def phase1_login(s: Session, username, password, label="user"): print(f"\n=== Phase 1b: Login ({label}) ===") csrf = s.csrf_headers() code, body, _ = s.post( f"{GATEWAY}/api/v1/auth/login", json_body={"username": username, "password": password}, headers=csrf, ) report(f"POST /auth/login ({label})", code, body) jtoken = s.get_cookie("JToken") print(f" JToken: {jtoken[:30]}..." if jtoken else " JToken: None") return body # ============================================================ # Phase 2: User endpoints (authenticated) # ============================================================ def phase2_user(s: Session, user_id): print("\n=== Phase 2: User Endpoints ===") csrf = s.csrf_headers() code, body, _ = s.get(f"{GATEWAY}/api/v1/users/me") report("GET /users/me", code, body) code, body, _ = s.put( f"{GATEWAY}/api/v1/users/me", json_body={"nickname": "TestNick", "bio": "testbio"}, headers=csrf, ) report("PUT /users/me", code, body) code, body, _ = s.put( f"{GATEWAY}/api/v1/users/me/preferences/notifications", json_body={"order": True, "community": True, "system": True}, headers=csrf, ) report("PUT /users/me/preferences/notifications", code, body) code, body, _ = s.put( f"{GATEWAY}/api/v1/users/me/preferences/theme", json_body={"theme": "dark"}, headers=csrf, ) report("PUT /users/me/preferences/theme", code, body) code, body, _ = s.get(f"{GATEWAY}/api/v1/users/{user_id}") report(f"GET /users/{user_id} (public)", code, body) # ============================================================ # Phase 3: Built-in Admin + Verification flow # ============================================================ def phase3_admin_and_verification( s_admin: Session, s_user: Session, s_reject: Session, admin_user, admin_pass, ): print("\n=== Phase 3: Built-in Admin & Verification ===") # Login admin via gateway s_admin.get(f"{GATEWAY}/healthz") # get CSRF csrf = s_admin.csrf_headers() code, body, _ = s_admin.post( f"{GATEWAY}/api/v1/auth/login", json_body={"username": admin_user, "password": admin_pass}, headers=csrf, ) report("POST /auth/login (admin)", code, body) code, body, _ = s_admin.get(f"{GATEWAY}/api/v1/users/me") report("GET /users/me (admin)", code, body) admin_id = pick_user_id(body) # User applies for player verification csrf_user = s_user.csrf_headers() code, body, _ = s_user.post( f"{GATEWAY}/api/v1/users/me/verification", json_body={ "role": "player", "materials": { "idCardFront": "http://example.com/front.jpg", "idCardBack": "http://example.com/back.jpg", "gameScreenshots": ["http://example.com/ss1.jpg"], "voiceDemo": "http://example.com/voice.mp3", }, }, headers=csrf_user, ) report("POST /users/me/verification (apply player)", code, body) # User applies for owner verification code, body, _ = s_user.post( f"{GATEWAY}/api/v1/users/me/verification", json_body={ "role": "owner", "materials": { "idCardFront": "http://example.com/front.jpg", "idCardBack": "http://example.com/back.jpg", "gameScreenshots": [], "voiceDemo": "", }, }, headers=csrf_user, ) report("POST /users/me/verification (apply owner)", code, body) # Another user applies so admin reject flow is covered too csrf_reject = s_reject.csrf_headers() code, body, _ = s_reject.post( f"{GATEWAY}/api/v1/users/me/verification", json_body={ "role": "owner", "materials": { "idCardFront": "http://example.com/reject-front.jpg", "idCardBack": "http://example.com/reject-back.jpg", "gameScreenshots": [], "voiceDemo": "", }, }, headers=csrf_reject, ) report("POST /users/me/verification (apply owner, reject user)", code, body) # Get my verifications code, body, _ = s_user.get(f"{GATEWAY}/api/v1/users/me/verification") report("GET /users/me/verification", code, body) user_verification_ids = {} for v in pick_items(body): user_verification_ids[v.get("role")] = as_int(v.get("id")) code, body, _ = s_reject.get(f"{GATEWAY}/api/v1/users/me/verification") report("GET /users/me/verification (reject user)", code, body) reject_verification_ids = {} for v in pick_items(body): reject_verification_ids[v.get("role")] = as_int(v.get("id")) # Admin: list and approve via gateway code, body, _ = s_admin.get(f"{GATEWAY}/api/v1/admin/verifications") report("GET /admin/verifications", code, body) # Admin: approve only the current test user's records for role in ("player", "owner"): vid = user_verification_ids.get(role) if not vid: continue code, body, _ = s_admin.post( f"{GATEWAY}/api/v1/admin/verifications/{vid}/approve", json_body={}, headers=s_admin.csrf_headers(), ) report( f"POST /admin/verifications/{vid}/approve ({role})", code, body, ) reject_vid = reject_verification_ids.get("owner") if reject_vid: code, body, _ = s_admin.post( f"{GATEWAY}/api/v1/admin/verifications/{reject_vid}/reject", json_body={"reason": "test reject flow"}, headers=s_admin.csrf_headers(), ) report( f"POST /admin/verifications/{reject_vid}/reject (owner)", code, body, ) code, body, _ = s_reject.get(f"{GATEWAY}/api/v1/users/me/verification") report("GET /users/me/verification (after reject)", code, body) # User: switch role to player code, body, _ = s_user.post( f"{GATEWAY}/api/v1/users/me/switch-role", json_body={"role": "player"}, headers=csrf_user, ) report("POST /users/me/switch-role (player)", code, body) return admin_id def phase3b_secondary_player(s_admin: Session, s_player: Session): print("\n=== Phase 3b: Secondary Player Setup ===") csrf_player = s_player.csrf_headers() code, body, _ = s_player.post( f"{GATEWAY}/api/v1/users/me/verification", json_body={ "role": "player", "materials": { "idCardFront": "http://example.com/player-front.jpg", "idCardBack": "http://example.com/player-back.jpg", "gameScreenshots": ["http://example.com/player-ss1.jpg"], "voiceDemo": "http://example.com/player-voice.mp3", }, }, headers=csrf_player, ) report("POST /users/me/verification (apply player, invited user)", code, body) code, body, _ = s_player.get(f"{GATEWAY}/api/v1/users/me/verification") report("GET /users/me/verification (invited user)", code, body) verification_id = 0 for item in pick_items(body): if item.get("role") == "player": verification_id = as_int(item.get("id")) break report_check( "lookup player verification id (invited user)", bool(verification_id), {"id": verification_id}, ) if verification_id: code, body, _ = s_admin.post( f"{GATEWAY}/api/v1/admin/verifications/{verification_id}/approve", json_body={}, headers=s_admin.csrf_headers(), ) report( f"POST /admin/verifications/{verification_id}/approve (invited user player)", code, body, ) code, body, _ = s_player.post( f"{GATEWAY}/api/v1/users/me/switch-role", json_body={"role": "player"}, headers=s_player.csrf_headers(), ) report("POST /users/me/switch-role (invited user player)", code, body) code, body, _ = s_player.post( f"{GATEWAY}/api/v1/players/me", json_body={}, headers=s_player.csrf_headers(), ) report("POST /players/me (invited user init)", code, body) return as_int(body.get("id")) def phase4_follow(s: Session, target_user_id): print("\n=== Phase 4: Follow/Unfollow ===") csrf = s.csrf_headers() code, body, _ = s.post( f"{GATEWAY}/api/v1/users/{target_user_id}/follow", json_body={}, headers=csrf, ) report(f"POST /users/{target_user_id}/follow", code, body) code, body, _ = s.delete( f"{GATEWAY}/api/v1/users/{target_user_id}/follow", headers=csrf, ) report(f"DELETE /users/{target_user_id}/follow", code, body) def phase5_games(s: Session, s_admin: Session): print("\n=== Phase 5: Games ===") code, body, _ = s.get(f"{GATEWAY}/api/v1/games") report("GET /games", code, body) csrf_admin = s_admin.csrf_headers() code, body, _ = s_admin.post( f"{GATEWAY}/api/v1/games", json_body={ "name": f"TestGame_{rand_str(4)}", "icon": "icon.png", "category": "MOBA", }, headers=csrf_admin, ) report("POST /games (create)", code, body) game_id = body.get("id", 0) code, body2, _ = s.get(f"{GATEWAY}/api/v1/games") report("GET /games (after create)", code, body2) items = pick_items(body2) if not game_id and items: game_id = as_int(items[-1].get("id")) if game_id: code, body, _ = s.get(f"{GATEWAY}/api/v1/games/{game_id}") report(f"GET /games/{game_id}", code, body) return game_id def phase6_player(s: Session, game_id): print("\n=== Phase 6: Player ===") csrf = s.csrf_headers() code, body, _ = s.post( f"{GATEWAY}/api/v1/players/me", json_body={}, headers=csrf, ) report("POST /players/me (init)", code, body) player_id = body.get("id", 0) code, body, _ = s.put( f"{GATEWAY}/api/v1/players/me/status", json_body={"status": "online"}, headers=csrf, ) report("PUT /players/me/status", code, body) code, body, _ = s.get(f"{GATEWAY}/api/v1/players") report("GET /players", code, body) if player_id: code, body, _ = s.get(f"{GATEWAY}/api/v1/players/{player_id}") report(f"GET /players/{player_id}", code, body) svc_body = None if game_id: code, body, _ = s.post( f"{GATEWAY}/api/v1/services", json_body={ "gameId": game_id, "title": "Boosting Service", "description": "Rank boost", "price": 50.0, "unit": "game", }, headers=csrf, ) report("POST /services (create)", code, body) svc_body = body code, body, _ = s.get(f"{GATEWAY}/api/v1/services") report("GET /services", code, body) service_id = svc_body.get("id", 0) if svc_body else 0 if service_id: code, body, _ = s.get(f"{GATEWAY}/api/v1/services/{service_id}") report(f"GET /services/{service_id}", code, body) code, body, _ = s.put( f"{GATEWAY}/api/v1/services/{service_id}", json_body={ "title": "Updated Service", "price": 60.0, "availability": ["weekday"], }, headers=csrf, ) report(f"PUT /services/{service_id}", code, body) if player_id: code, body, _ = s.get(f"{GATEWAY}/api/v1/players/{player_id}/services") report(f"GET /players/{player_id}/services", code, body) return player_id, service_id def phase7_shop(s_owner: Session, s_invited_player: Session, owner_user_id, invited_player_id): print("\n=== Phase 7: Shop ===") s_owner.post( f"{GATEWAY}/api/v1/users/me/switch-role", json_body={"role": "owner"}, headers=s_owner.csrf_headers(), ) csrf = s_owner.csrf_headers() code, body, _ = s_owner.post( f"{GATEWAY}/api/v1/shops", json_body={ "name": f"TestShop_{rand_str(4)}", "description": "A test shop", "commissionType": "percentage", "commissionValue": "10", }, headers=csrf, ) report("POST /shops (create)", code, body) shop_id_str = body.get("id", "0") try: shop_id = int(shop_id_str) except (ValueError, TypeError): shop_id = 0 code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops") report("GET /shops", code, body) if shop_id: code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/{shop_id}") report(f"GET /shops/{shop_id}", code, body) code, body, _ = s_owner.put( f"{GATEWAY}/api/v1/shops/{shop_id}", json_body={ "name": f"UpdatedShop_{rand_str(4)}", "description": "An updated test shop", "commissionType": "percentage", "commissionValue": "12", "allowMultiShop": True, "allowIndependentOrders": False, "dispatchMode": "manual", }, headers=csrf, ) report(f"PUT /shops/{shop_id}", code, body) code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/users/{owner_user_id}/shop") report(f"GET /users/{owner_user_id}/shop", code, body) announcement = f"Grand opening {rand_str(4)}!" code, body, _ = s_owner.post( f"{GATEWAY}/api/v1/shops/{shop_id}/announcements", json_body={"content": announcement}, headers=csrf, ) report(f"POST /shops/{shop_id}/announcements", code, body) code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/{shop_id}") report(f"GET /shops/{shop_id} (after announcement)", code, body) announcement_index = -1 announcements = body.get("announcements") if isinstance(body, dict) else None if isinstance(announcements, list): for idx in range(len(announcements) - 1, -1, -1): if announcement in str(announcements[idx]): announcement_index = idx break report_check( f"locate announcement index ({shop_id})", announcement_index >= 0, {"index": announcement_index}, ) if announcement_index >= 0: code, body, _ = s_owner.delete( f"{GATEWAY}/api/v1/shops/{shop_id}/announcements/{announcement_index}", headers=csrf, ) report( f"DELETE /shops/{shop_id}/announcements/{announcement_index}", code, body, ) code, body, _ = s_owner.put( f"{GATEWAY}/api/v1/shops/{shop_id}/template", json_body={"sections": json.dumps({"layout": "grid", "theme": "dark"})}, headers=csrf, ) report(f"PUT /shops/{shop_id}/template", code, body) code, body, _ = s_owner.get( f"{GATEWAY}/api/v1/shops/{shop_id}/income-stats", ) report(f"GET /shops/{shop_id}/income-stats", code, body) if invited_player_id: code, body, _ = s_owner.post( f"{GATEWAY}/api/v1/shops/{shop_id}/invitations", json_body={"playerId": invited_player_id}, headers=csrf, ) report(f"POST /shops/{shop_id}/invitations", code, body) code, body, _ = s_owner.get( f"{GATEWAY}/api/v1/shops/{shop_id}/invitations", ) report(f"GET /shops/{shop_id}/invitations", code, body) invitation_id = 0 for item in pick_items(body): if as_int(item.get("playerId")) == invited_player_id: invitation_id = as_int(item.get("id")) break report_check( "locate invitation id", bool(invitation_id), {"id": invitation_id}, ) s_invited_player.post( f"{GATEWAY}/api/v1/users/me/switch-role", json_body={"role": "player"}, headers=s_invited_player.csrf_headers(), ) code, body, _ = s_invited_player.get( f"{GATEWAY}/api/v1/shops/invitations/mine", ) report("GET /shops/invitations/mine", code, body) if invitation_id: inv_csrf = s_invited_player.csrf_headers() code, body, _ = s_invited_player.post( f"{GATEWAY}/api/v1/shops/invitations/{invitation_id}/accept", json_body={}, headers=inv_csrf, ) report( f"POST /shops/invitations/{invitation_id}/accept", code, body, ) code, body, _ = s_owner.delete( f"{GATEWAY}/api/v1/shops/{shop_id}/players/{invited_player_id}", headers=csrf, ) report( f"DELETE /shops/{shop_id}/players/{invited_player_id}", code, body, ) code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/mine") report("GET /shops/mine", code, body) return shop_id def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, shop_id): print("\n=== Phase 8: Orders ===") s_consumer.post( f"{GATEWAY}/api/v1/users/me/switch-role", json_body={"role": "consumer"}, headers=s_consumer.csrf_headers(), ) csrf = s_consumer.csrf_headers() code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders", json_body={ "playerId": player_id, "serviceId": service_id, "shopId": shop_id, "quantity": 1, "note": "test order", }, headers=csrf, ) report("POST /orders (create)", code, body) order_obj = body.get("order", {}) order_id = order_obj.get("id", 0) if isinstance(order_obj, dict) else 0 code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders?role=consumer") report("GET /orders?role=consumer", code, body) code, body, _ = s_actor.get(f"{GATEWAY}/api/v1/orders?role=player") report("GET /orders?role=player", code, body) code, body, _ = s_actor.get(f"{GATEWAY}/api/v1/orders?role=owner") report("GET /orders?role=owner", code, body) if order_id: code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders/{order_id}") report(f"GET /orders/{order_id}", code, body) code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders/{order_id}/pay", json_body={}, headers=csrf, ) report(f"POST /orders/{order_id}/pay", code, body) code, body, _ = s_actor.post( f"{GATEWAY}/api/v1/orders/{order_id}/accept", json_body={}, headers=s_actor.csrf_headers(), ) report(f"POST /orders/{order_id}/accept", code, body) code, body, _ = s_actor.post( f"{GATEWAY}/api/v1/orders/{order_id}/request-close", json_body={}, headers=s_actor.csrf_headers(), ) report(f"POST /orders/{order_id}/request-close", code, body) code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders/{order_id}/confirm-close", json_body={}, headers=csrf, ) report(f"POST /orders/{order_id}/confirm-close", code, body) code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders", json_body={ "playerId": player_id, "serviceId": service_id, "quantity": 1, }, headers=csrf, ) order2 = body.get("order", {}) order2_id = order2.get("id", 0) if isinstance(order2, dict) else 0 if order_id: code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders/{order_id}/reorder", json_body={}, headers=csrf, ) report(f"POST /orders/{order_id}/reorder", code, body) if order2_id: code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders/{order2_id}/cancel", json_body={}, headers=csrf, ) report(f"POST /orders/{order2_id}/cancel", code, body) code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders/paid", json_body={ "playerId": player_id, "serviceId": service_id, "quantity": 1, }, headers=csrf, ) report("POST /orders/paid (create+pay)", code, body) return order_id def phase9_wallet(s: Session): print("\n=== Phase 9: Wallet ===") csrf = s.csrf_headers() code, body, _ = s.get(f"{GATEWAY}/api/v1/wallet/balance") report("GET /wallet/balance", code, body) code, body, _ = s.post( f"{GATEWAY}/api/v1/wallet/topup", json_body={"amount": "100.00", "method": "alipay"}, headers=csrf, ) report("POST /wallet/topup", code, body) code, body, _ = s.post( f"{GATEWAY}/api/v1/wallet/withdraw", json_body={"amount": "10.00", "method": "alipay"}, headers=csrf, ) report("POST /wallet/withdraw", code, body) code, body, _ = s.get(f"{GATEWAY}/api/v1/wallet/transactions") report("GET /wallet/transactions", code, body) def phase10_community(s: Session, user_id): print("\n=== Phase 10: Community ===") csrf = s.csrf_headers() code, body, _ = s.post( f"{GATEWAY}/api/v1/posts", json_body={ "title": "Test Post", "content": "Hello world", "images": [], "tags": ["test"], }, headers=csrf, ) report("POST /posts (create)", code, body) post_id = body.get("id", 0) code, body, _ = s.get(f"{GATEWAY}/api/v1/posts") report("GET /posts", code, body) if post_id: code, body, _ = s.get(f"{GATEWAY}/api/v1/posts/{post_id}") report(f"GET /posts/{post_id}", code, body) code, body, _ = s.post( f"{GATEWAY}/api/v1/posts/{post_id}/like", json_body={}, headers=csrf, ) report(f"POST /posts/{post_id}/like", code, body) code, body, _ = s.delete( f"{GATEWAY}/api/v1/posts/{post_id}/like", headers=csrf, ) report(f"DELETE /posts/{post_id}/like", code, body) code, body, _ = s.post( f"{GATEWAY}/api/v1/posts/{post_id}/pin", json_body={}, headers=csrf, ) report(f"POST /posts/{post_id}/pin", code, body) code, body, _ = s.delete( f"{GATEWAY}/api/v1/posts/{post_id}/pin", headers=csrf, ) report(f"DELETE /posts/{post_id}/pin", code, body) code, body, _ = s.post( f"{GATEWAY}/api/v1/posts/{post_id}/comments", json_body={"content": "Nice post!"}, headers=csrf, ) report(f"POST /posts/{post_id}/comments", code, body) comment_id = body.get("id", 0) code, body, _ = s.get(f"{GATEWAY}/api/v1/posts/{post_id}/comments") report(f"GET /posts/{post_id}/comments", code, body) if comment_id: code, body, _ = s.post( f"{GATEWAY}/api/v1/comments/{comment_id}/like", json_body={}, headers=csrf, ) report(f"POST /comments/{comment_id}/like", code, body) code, body, _ = s.delete( f"{GATEWAY}/api/v1/comments/{comment_id}/like", headers=csrf, ) report(f"DELETE /comments/{comment_id}/like", code, body) code, body, _ = s.get(f"{GATEWAY}/api/v1/users/{user_id}/posts") report(f"GET /users/{user_id}/posts", code, body) return post_id def phase11_objectstory(s: Session): print("\n=== Phase 11: Objectstory (File) ===") code, body, _ = s.post_multipart( f"{GATEWAY}/api/v1/upload", fields={"type": "post"}, files={ "file": ( f"test-{rand_str(4)}.txt", f"juwan-objectstory-{rand_str(8)}", "text/plain", ) }, headers=s.csrf_headers(), ) report("POST /upload", code, body) if code == 200: report_check( "POST /upload returned url", bool(body.get("url")), {"url": body.get("url", "")}, ) code, body, _ = s.get(f"{GATEWAY}/api/v1/files?key=nonexistent") report( "GET /files?key=nonexistent (expect error)", code, body, expect_status=(400, 500), ) return def phase12_email(s: Session): print("\n=== Phase 12: Email ===") csrf = s.csrf_headers() code, body, _ = s.post( f"{GATEWAY}/api/v1/email/verification-code/send", json_body={"email": f"test_{rand_str(4)}@example.com", "scene": "register"}, headers=csrf, ) report("POST /email/verification-code/send (gateway)", code, body) code, body, _ = s.post( f"{GATEWAY}/api/v1/auth/forgot-password/send", json_body={"email": f"test_{rand_str(4)}@example.com"}, headers=csrf, ) report("POST /auth/forgot-password/send (gateway)", code, body) def phase13_logout(s: Session): print("\n=== Phase 13: Logout ===") csrf = s.csrf_headers() code, body, _ = s.post( f"{GATEWAY}/api/v1/auth/logout", json_body={}, headers=csrf, ) report("POST /auth/logout", code, body) def phase14_reset_password(username, email, new_password): print("\n=== Phase 14: Forgot/Reset Password ===") s_reset = Session() s_reset.get(f"{GATEWAY}/healthz") csrf = s_reset.csrf_headers() code, body, _ = s_reset.post( f"{GATEWAY}/api/v1/auth/forgot-password/send", json_body={"email": email}, headers=csrf, ) report("POST /auth/forgot-password/send", code, body) request_id = body.get("requestId", "") if not request_id: print(" [WARN] No requestId returned, skip reset-password") return vcode = read_vcode_from_redis(request_id, "reset_password", email) if not vcode: print(" [WARN] No reset password verification code found in Redis") return csrf["X-Request-Id"] = request_id code, body, _ = s_reset.post( f"{GATEWAY}/api/v1/auth/reset-password", json_body={ "email": email, "vcode": vcode, "newPassword": new_password, }, headers=csrf, ) report("POST /auth/reset-password", code, body) if code != 200: print(" [SKIP] Reset password failed, skip login verification") return s_login = Session() s_login.get(f"{GATEWAY}/healthz") code, body, _ = s_login.post( f"{GATEWAY}/api/v1/auth/login", json_body={"username": username, "password": new_password}, headers=s_login.csrf_headers(), ) report("POST /auth/login (after reset)", code, body) def phase15_player_service_delete(s: Session, service_id): print("\n=== Phase 15: Delete Service ===") if not service_id: print(" [SKIP] No service to delete") return csrf = s.csrf_headers() code, body, _ = s.delete( f"{GATEWAY}/api/v1/services/{service_id}", headers=csrf, ) report(f"DELETE /services/{service_id}", code, body) def main(): global passed, failed suffix = rand_str(6) user1_name = f"testuser_{suffix}" user1_email = f"testuser_{suffix}@example.com" user1_pass = "TestPass123!" consumer_name = f"consumer_{suffix}" consumer_email = f"consumer_{suffix}@example.com" consumer_pass = "ConsumerPass123!" consumer_new_pass = "ConsumerPass456!" admin_name = ADMIN_USERNAME admin_pass = ADMIN_PASSWORD print(f"Test run: user={user1_name}, admin={admin_name}") s_user = Session() s_admin = Session() s_consumer = Session() phase0_health(s_user) phase1_register(s_user, user1_name, user1_email, user1_pass, label="primary") login_resp = phase1_login(s_user, user1_name, user1_pass, label="primary") user_id = pick_user_id(login_resp) print(f" User ID: {user_id}") phase2_user(s_user, user_id) s_consumer.get(f"{GATEWAY}/healthz") phase1_register(s_consumer, consumer_name, consumer_email, consumer_pass, label="consumer") consumer_login_resp = phase1_login( s_consumer, consumer_name, consumer_pass, label="consumer", ) consumer_user_id = pick_user_id(consumer_login_resp) print(f" Consumer User ID: {consumer_user_id}") admin_id = phase3_admin_and_verification( s_admin, s_user, s_consumer, admin_name, admin_pass, ) invited_player_id = phase3b_secondary_player(s_admin, s_consumer) if admin_id and user_id: phase4_follow(s_user, admin_id) s_user.post( f"{GATEWAY}/api/v1/users/me/switch-role", json_body={"role": "player"}, headers=s_user.csrf_headers(), ) game_id = phase5_games(s_user, s_admin) player_id, service_id = phase6_player(s_user, game_id) shop_id = phase7_shop(s_user, s_consumer, user_id, invited_player_id) phase8_order(s_consumer, s_user, player_id, service_id, shop_id) phase9_wallet(s_user) phase10_community(s_user, user_id) phase11_objectstory(s_user) phase12_email(s_user) phase14_reset_password(consumer_name, consumer_email, consumer_new_pass) phase15_player_service_delete(s_user, service_id) phase13_logout(s_user) print(f"\n{'=' * 60}") print(f"RESULTS: {passed} passed, {failed} failed, {passed + failed} total") print(f"{'=' * 60}") if errors_list: print("\nFailed tests:") for name, status, body in errors_list: body_s = json.dumps(body, ensure_ascii=False) if len(body_s) > 300: body_s = body_s[:300] + "..." print(f" - {name}: HTTP {status} {body_s}") sys.exit(1 if failed else 0) if __name__ == "__main__": main()