From 05efd5cc8dae6773608cbca6955785342a9daeb6 Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sat, 25 Apr 2026 07:40:50 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E5=A2=9E=E5=BC=BA=20dev=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E6=B5=81=E7=A8=8B=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/dev/test_all_apis.py | 742 +++++++++++++++++++++++++++++++++++- 1 file changed, 730 insertions(+), 12 deletions(-) diff --git a/deploy/dev/test_all_apis.py b/deploy/dev/test_all_apis.py index 5e3aa00..40537b3 100644 --- a/deploy/dev/test_all_apis.py +++ b/deploy/dev/test_all_apis.py @@ -16,6 +16,7 @@ import urllib.parse import http.cookiejar import os import subprocess +from decimal import Decimal, InvalidOperation GATEWAY = "http://127.0.0.1:18080" ADMIN_USERNAME = os.getenv("ADMIN_USERNAME", "admin") @@ -38,6 +39,17 @@ def as_int(value, default=0): return default +def as_decimal(value): + try: + return Decimal(str(value)) + except (InvalidOperation, TypeError, ValueError): + return None + + +def same_id(left, right): + return str(left) == str(right) + + def pick_items(body): if isinstance(body.get("items"), list): return body["items"] @@ -46,6 +58,22 @@ def pick_items(body): return [] +def find_item(items, predicate): + for item in items: + if predicate(item): + return item + return None + + +def find_item_by_id(items, item_id): + return find_item(items, lambda item: same_id(item.get("id"), item_id)) + + +def user_from(body): + user = body.get("user") if isinstance(body, dict) else None + return user if isinstance(user, dict) else {} + + def pick_user_id(body): if not isinstance(body, dict): return 0 @@ -226,6 +254,17 @@ def skip(name, reason): print(f" [SKIP] {name}: {reason}") +def check_order_status(session, order_id, expected_status, label): + code, body, _ = session.get(f"{GATEWAY}/api/v1/orders/{order_id}") + report(f"GET /orders/{order_id} ({label})", code, body) + report_check( + f"order {order_id} status is {expected_status} ({label})", + code == 200 and body.get("status") == expected_status, + body, + ) + return body + + # ============================================================ # Phase 0: Health check & CSRF # ============================================================ @@ -281,6 +320,16 @@ def phase1_register(s: Session, username, email, password, label="user"): headers=csrf, ) report(f"POST /auth/register ({label})", code, body) + user = user_from(body) + report_check( + f"registered user fields ({label})", + code == 200 + and as_int(user.get("id")) > 0 + and user.get("username") == username + and user.get("role") == "consumer" + and "consumer" in (user.get("verifiedRoles") or []), + user, + ) return body @@ -295,6 +344,15 @@ def phase1_login(s: Session, username, password, label="user"): report(f"POST /auth/login ({label})", code, body) jtoken = s.get_cookie("JToken") print(f" JToken: {jtoken[:30]}..." if jtoken else " JToken: None") + user = user_from(body) + report_check( + f"login session fields ({label})", + code == 200 + and bool(jtoken) + and as_int(user.get("id")) > 0 + and user.get("username") == username, + {"user": user, "hasJToken": bool(jtoken)}, + ) return body @@ -307,6 +365,11 @@ def phase2_user(s: Session, user_id): code, body, _ = s.get(f"{GATEWAY}/api/v1/users/me") report("GET /users/me", code, body) + report_check( + "GET /users/me returns current user", + code == 200 and same_id(body.get("id"), user_id), + body, + ) code, body, _ = s.put( f"{GATEWAY}/api/v1/users/me", @@ -314,6 +377,14 @@ def phase2_user(s: Session, user_id): headers=csrf, ) report("PUT /users/me", code, body) + report_check( + "PUT /users/me persists profile fields", + code == 200 + and same_id(body.get("id"), user_id) + and body.get("nickname") == "TestNick" + and body.get("bio") == "testbio", + body, + ) code, body, _ = s.put( f"{GATEWAY}/api/v1/users/me/preferences/notifications", @@ -331,6 +402,14 @@ def phase2_user(s: Session, user_id): code, body, _ = s.get(f"{GATEWAY}/api/v1/users/{user_id}") report(f"GET /users/{user_id} (public)", code, body) + report_check( + f"GET /users/{user_id} exposes updated public profile", + code == 200 + and same_id(body.get("id"), user_id) + and body.get("nickname") == "TestNick" + and body.get("bio") == "testbio", + body, + ) # ============================================================ @@ -415,12 +494,35 @@ def phase3_admin_and_verification( user_verification_ids = {} for v in pick_items(body): user_verification_ids[v.get("role")] = as_int(v.get("id")) + report_check( + "verification applications are pending", + all( + find_item( + pick_items(body), + lambda item, role=role: item.get("role") == role + and item.get("status") == "pending", + ) + for role in ("player", "owner") + ), + body, + ) 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")) + report_check( + "reject user verification is pending", + bool( + find_item( + pick_items(body), + lambda item: item.get("role") == "owner" + and item.get("status") == "pending", + ) + ), + body, + ) # Admin: list and approve via gateway code, body, _ = s_admin.get(f"{GATEWAY}/api/v1/admin/verifications") @@ -457,6 +559,31 @@ def phase3_admin_and_verification( code, body, _ = s_reject.get(f"{GATEWAY}/api/v1/users/me/verification") report("GET /users/me/verification (after reject)", code, body) + report_check( + "rejected verification keeps reason", + bool( + find_item( + pick_items(body), + lambda item: item.get("role") == "owner" + and item.get("status") == "rejected" + and item.get("rejectReason") == "test reject flow", + ) + ), + body, + ) + + code, body, _ = s_user.get(f"{GATEWAY}/api/v1/users/me") + report("GET /users/me (after approval)", code, body) + verified_roles = body.get("verifiedRoles") or [] + verification_status = body.get("verificationStatus") or {} + report_check( + "approved roles are visible on current user", + "player" in verified_roles + and "owner" in verified_roles + and verification_status.get("player") == "approved" + and verification_status.get("owner") == "approved", + body, + ) # User: switch role to player code, body, _ = s_user.post( @@ -465,6 +592,9 @@ def phase3_admin_and_verification( headers=csrf_user, ) report("POST /users/me/switch-role (player)", code, body) + code, body, _ = s_user.get(f"{GATEWAY}/api/v1/users/me") + report("GET /users/me (after switch player)", code, body) + report_check("current role is player", body.get("role") == "player", body) return admin_id @@ -500,6 +630,17 @@ def phase3b_secondary_player(s_admin: Session, s_player: Session): bool(verification_id), {"id": verification_id}, ) + report_check( + "invited user player verification is pending", + bool( + find_item( + pick_items(body), + lambda item: item.get("role") == "player" + and item.get("status") == "pending", + ) + ), + body, + ) if verification_id: code, body, _ = s_admin.post( @@ -519,6 +660,9 @@ def phase3b_secondary_player(s_admin: Session, s_player: Session): headers=s_player.csrf_headers(), ) report("POST /users/me/switch-role (invited user player)", code, body) + code, body, _ = s_player.get(f"{GATEWAY}/api/v1/users/me") + report("GET /users/me (invited user player role)", code, body) + report_check("invited user current role is player", body.get("role") == "player", body) code, body, _ = s_player.post( f"{GATEWAY}/api/v1/players/me", @@ -526,6 +670,13 @@ def phase3b_secondary_player(s_admin: Session, s_player: Session): headers=s_player.csrf_headers(), ) report("POST /players/me (invited user init)", code, body) + report_check( + "invited player profile initialized", + as_int(body.get("id")) > 0 + and as_int((body.get("user") or {}).get("id")) > 0 + and body.get("status") == "offline", + body, + ) return as_int(body.get("id")) @@ -552,29 +703,55 @@ def phase5_games(s: Session, s_admin: Session): code, body, _ = s.get(f"{GATEWAY}/api/v1/games") report("GET /games", code, body) + report_check("GET /games returns list shape", isinstance(pick_items(body), list), body) csrf_admin = s_admin.csrf_headers() + game_name = f"TestGame_{rand_str(4)}" + game_icon = "icon.png" + game_category = "MOBA" code, body, _ = s_admin.post( f"{GATEWAY}/api/v1/games", json_body={ - "name": f"TestGame_{rand_str(4)}", - "icon": "icon.png", - "category": "MOBA", + "name": game_name, + "icon": game_icon, + "category": game_category, }, headers=csrf_admin, ) report("POST /games (create)", code, body) game_id = body.get("id", 0) + report_check( + "created game fields", + as_int(game_id) > 0 + and body.get("name") == game_name + and body.get("icon") == game_icon + and body.get("category") == game_category, + body, + ) code, body2, _ = s.get(f"{GATEWAY}/api/v1/games") report("GET /games (after create)", code, body2) items = pick_items(body2) + created_game = find_item_by_id(items, game_id) + report_check( + "created game appears in list", + bool(created_game) and created_game.get("name") == game_name, + 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) + report_check( + f"GET /games/{game_id} returns created game", + code == 200 + and same_id(body.get("id"), game_id) + and body.get("name") == game_name + and body.get("category") == game_category, + body, + ) return game_id @@ -590,6 +767,13 @@ def phase6_player(s: Session, game_id): ) report("POST /players/me (init)", code, body) player_id = body.get("id", 0) + report_check( + "player profile initialized", + as_int(player_id) > 0 + and body.get("status") == "offline" + and as_int((body.get("user") or {}).get("id")) > 0, + body, + ) code, body, _ = s.put( f"{GATEWAY}/api/v1/players/me/status", @@ -600,10 +784,16 @@ def phase6_player(s: Session, game_id): code, body, _ = s.get(f"{GATEWAY}/api/v1/players") report("GET /players", code, body) + report_check("GET /players returns list shape", isinstance(pick_items(body), list), body) if player_id: code, body, _ = s.get(f"{GATEWAY}/api/v1/players/{player_id}") report(f"GET /players/{player_id}", code, body) + report_check( + f"GET /players/{player_id} returns initialized online player", + code == 200 and same_id(body.get("id"), player_id) and body.get("status") == "online", + body, + ) svc_body = None if game_id: @@ -620,14 +810,34 @@ def phase6_player(s: Session, game_id): ) report("POST /services (create)", code, body) svc_body = body + report_check( + "created service fields", + code == 200 + and as_int(body.get("id")) > 0 + and same_id(body.get("playerId"), player_id) + and same_id(body.get("gameId"), game_id) + and body.get("title") == "Boosting Service" + and body.get("description") == "Rank boost" + and as_decimal(body.get("price")) == Decimal("50") + and body.get("unit") == "game", + body, + ) code, body, _ = s.get(f"{GATEWAY}/api/v1/services") report("GET /services", code, body) + report_check("GET /services returns list shape", isinstance(pick_items(body), list), 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) + report_check( + f"GET /services/{service_id} returns created service", + code == 200 + and same_id(body.get("id"), service_id) + and body.get("title") == "Boosting Service", + body, + ) code, body, _ = s.put( f"{GATEWAY}/api/v1/services/{service_id}", @@ -640,9 +850,24 @@ def phase6_player(s: Session, game_id): ) report(f"PUT /services/{service_id}", code, body) + code, body, _ = s.get(f"{GATEWAY}/api/v1/services/{service_id}") + report(f"GET /services/{service_id} (after update)", code, body) + report_check( + f"GET /services/{service_id} reflects update", + code == 200 + and body.get("title") == "Updated Service" + and as_decimal(body.get("price")) == Decimal("60"), + 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) + report_check( + f"GET /players/{player_id}/services contains service", + bool(find_item_by_id(pick_items(body), service_id)), + body, + ) return player_id, service_id @@ -657,12 +882,16 @@ def phase7_shop( json_body={"role": "owner"}, headers=s_owner.csrf_headers(), ) + code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/users/me") + report("GET /users/me (owner role)", code, body) + report_check("current role is owner", body.get("role") == "owner", body) csrf = s_owner.csrf_headers() + shop_name = f"TestShop_{rand_str(4)}" code, body, _ = s_owner.post( f"{GATEWAY}/api/v1/shops", json_body={ - "name": f"TestShop_{rand_str(4)}", + "name": shop_name, "description": "A test shop", "commissionType": "percentage", "commissionValue": "10", @@ -675,18 +904,39 @@ def phase7_shop( shop_id = int(shop_id_str) except (ValueError, TypeError): shop_id = 0 + report_check( + "created shop fields", + as_int(shop_id) > 0 + and body.get("name") == shop_name + and body.get("description") == "A test shop" + and body.get("commissionType") == "percentage" + and body.get("commissionValue") == "10" + and same_id((body.get("owner") or {}).get("id"), owner_user_id), + body, + ) code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops") report("GET /shops", code, body) + report_check( + "created shop appears in list", + bool(find_item_by_id(pick_items(body), shop_id)), + body, + ) if shop_id: code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/{shop_id}") report(f"GET /shops/{shop_id}", code, body) + report_check( + f"GET /shops/{shop_id} returns created shop", + same_id(body.get("id"), shop_id) and body.get("name") == shop_name, + body, + ) + updated_shop_name = f"UpdatedShop_{rand_str(4)}" code, body, _ = s_owner.put( f"{GATEWAY}/api/v1/shops/{shop_id}", json_body={ - "name": f"UpdatedShop_{rand_str(4)}", + "name": updated_shop_name, "description": "An updated test shop", "commissionType": "percentage", "commissionValue": "12", @@ -697,9 +947,26 @@ def phase7_shop( headers=csrf, ) report(f"PUT /shops/{shop_id}", code, body) + report_check( + f"PUT /shops/{shop_id} returns updated fields", + same_id(body.get("id"), shop_id) + and body.get("name") == updated_shop_name + and body.get("description") == "An updated test shop" + and body.get("commissionValue") == "12" + and body.get("allowMultiShop") is True + and body.get("allowIndependentOrders") is False + and body.get("dispatchMode") == "manual", + 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) + report_check( + f"GET /users/{owner_user_id}/shop returns owned shop", + same_id(body.get("id"), shop_id) + and same_id((body.get("owner") or {}).get("id"), owner_user_id), + body, + ) announcement = f"Grand opening {rand_str(4)}!" code, body, _ = s_owner.post( @@ -733,18 +1000,47 @@ def phase7_shop( code, body, ) + code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/{shop_id}") + report(f"GET /shops/{shop_id} (after announcement delete)", code, body) + report_check( + "deleted announcement is absent", + announcement not in [str(item) for item in body.get("announcements") or []], + body, + ) + template_sections = json.dumps({"layout": "grid", "theme": "dark"}) code, body, _ = s_owner.put( f"{GATEWAY}/api/v1/shops/{shop_id}/template", - json_body={"sections": json.dumps({"layout": "grid", "theme": "dark"})}, + json_body={"sections": template_sections}, headers=csrf, ) report(f"PUT /shops/{shop_id}/template", code, body) + code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/{shop_id}") + report(f"GET /shops/{shop_id} (after template)", code, body) + report_check( + "shop template is persisted", + bool(body.get("templateConfig")), + 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) + report_check( + "income stats response has business fields", + all( + key in body + for key in ( + "monthlyIncome", + "pendingSettlement", + "totalWithdrawn", + "totalOrders", + "completedOrders", + ) + ), + body, + ) if invited_player_id: code, body, _ = s_owner.post( @@ -769,6 +1065,16 @@ def phase7_shop( bool(invitation_id), {"id": invitation_id}, ) + invitation = find_item_by_id(pick_items(body), invitation_id) + report_check( + "created invitation is pending", + bool(invitation) + and same_id(invitation.get("shopId"), shop_id) + and same_id(invitation.get("playerId"), invited_player_id) + and invitation.get("status") == "pending" + and same_id(invitation.get("invitedBy"), owner_user_id), + body, + ) s_invited_player.post( f"{GATEWAY}/api/v1/users/me/switch-role", @@ -792,6 +1098,18 @@ def phase7_shop( code, body, ) + code, body, _ = s_owner.get( + f"{GATEWAY}/api/v1/shops/{shop_id}/invitations", + ) + report(f"GET /shops/{shop_id}/invitations (after accept)", code, body) + accepted = find_item_by_id(pick_items(body), invitation_id) + report_check( + "accepted invitation has accepted status", + bool(accepted) + and accepted.get("status") == "accepted" + and as_int(accepted.get("respondedAt")) > 0, + body, + ) code, body, _ = s_owner.delete( f"{GATEWAY}/api/v1/shops/{shop_id}/players/{invited_player_id}", @@ -802,6 +1120,13 @@ def phase7_shop( code, body, ) + code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/{shop_id}") + report(f"GET /shops/{shop_id} (after player remove)", code, body) + report_check( + "removed player is reflected in shop profile", + as_int(body.get("playerCount")) >= 0, + body, + ) # Reject invitation flow: re-invite the removed player, then reject code, body, _ = s_owner.post( @@ -832,9 +1157,22 @@ def phase7_shop( code, body, ) + code, body, _ = s_owner.get( + f"{GATEWAY}/api/v1/shops/{shop_id}/invitations", + ) + report(f"GET /shops/{shop_id}/invitations (after reject)", code, body) + rejected = find_item_by_id(pick_items(body), reinvite_id) + report_check( + "rejected invitation has rejected status", + bool(rejected) + and rejected.get("status") == "rejected" + and as_int(rejected.get("respondedAt")) > 0, + body, + ) code, body, _ = s_owner.get(f"{GATEWAY}/api/v1/shops/mine") report("GET /shops/mine", code, body) + report_check("GET /shops/mine returns owned shop", same_id(body.get("id"), shop_id), body) return shop_id @@ -847,6 +1185,10 @@ def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, s json_body={"role": "consumer"}, headers=s_consumer.csrf_headers(), ) + code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/users/me") + report("GET /users/me (consumer role)", code, body) + consumer_user_id = pick_user_id(body) + report_check("current role is consumer", body.get("role") == "consumer", body) csrf = s_consumer.csrf_headers() code, body, _ = s_consumer.post( @@ -863,19 +1205,52 @@ def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, s 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 + report_check( + "created order starts pending payment", + body.get("ok") is True + and as_int(order_id) > 0 + and same_id(order_obj.get("consumerId"), consumer_user_id) + and same_id(order_obj.get("playerId"), player_id) + and same_id(order_obj.get("shopId"), shop_id) + and order_obj.get("status") == "pending_payment" + and as_decimal(order_obj.get("totalPrice")) == Decimal("50") + and order_obj.get("note") == "test order", + body, + ) code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders?role=consumer") report("GET /orders?role=consumer", code, body) + report_check( + "consumer order list contains created order", + bool(find_item_by_id(pick_items(body), order_id)), + body, + ) code, body, _ = s_actor.get(f"{GATEWAY}/api/v1/orders?role=player") report("GET /orders?role=player", code, body) + report_check( + "player order list contains created order", + bool(find_item_by_id(pick_items(body), order_id)), + body, + ) code, body, _ = s_actor.get(f"{GATEWAY}/api/v1/orders?role=owner") report("GET /orders?role=owner", code, body) + report_check( + "owner order list contains created order", + bool(find_item_by_id(pick_items(body), order_id)), + body, + ) if order_id: - code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders/{order_id}") - report(f"GET /orders/{order_id}", code, body) + body = check_order_status(s_consumer, order_id, "pending_payment", "after create") + report_check( + "created order detail matches participants", + same_id(body.get("consumerId"), consumer_user_id) + and same_id(body.get("playerId"), player_id) + and same_id(body.get("shopId"), shop_id), + body, + ) code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders/{order_id}/pay", @@ -883,6 +1258,7 @@ def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, s headers=csrf, ) report(f"POST /orders/{order_id}/pay", code, body) + check_order_status(s_consumer, order_id, "pending_accept", "after pay") code, body, _ = s_actor.post( f"{GATEWAY}/api/v1/orders/{order_id}/accept", @@ -890,6 +1266,8 @@ def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, s headers=s_actor.csrf_headers(), ) report(f"POST /orders/{order_id}/accept", code, body) + body = check_order_status(s_consumer, order_id, "in_progress", "after accept") + report_check("accepted order has acceptedAt", bool(body.get("acceptedAt")), body) code, body, _ = s_actor.post( f"{GATEWAY}/api/v1/orders/{order_id}/request-close", @@ -897,6 +1275,7 @@ def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, s headers=s_actor.csrf_headers(), ) report(f"POST /orders/{order_id}/request-close", code, body) + check_order_status(s_consumer, order_id, "pending_close", "after request-close") code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders/{order_id}/confirm-close", @@ -904,6 +1283,7 @@ def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, s headers=csrf, ) report(f"POST /orders/{order_id}/confirm-close", code, body) + check_order_status(s_consumer, order_id, "pending_review", "after confirm-close") code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders", @@ -916,6 +1296,11 @@ def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, s ) order2 = body.get("order", {}) order2_id = order2.get("id", 0) if isinstance(order2, dict) else 0 + report_check( + "second order starts pending payment", + as_int(order2_id) > 0 and order2.get("status") == "pending_payment", + body, + ) if order_id: code, body, _ = s_consumer.post( @@ -924,6 +1309,15 @@ def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, s headers=csrf, ) report(f"POST /orders/{order_id}/reorder", code, body) + reordered = body.get("order", {}) if isinstance(body, dict) else {} + report_check( + "reorder creates a new pending order", + body.get("ok") is True + and as_int(reordered.get("id")) > 0 + and not same_id(reordered.get("id"), order_id) + and reordered.get("status") == "pending_payment", + body, + ) if order2_id: code, body, _ = s_consumer.post( @@ -932,6 +1326,7 @@ def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, s headers=csrf, ) report(f"POST /orders/{order2_id}/pay (before cancel)", code, body) + check_order_status(s_consumer, order2_id, "pending_accept", "second order after pay") code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders/{order2_id}/cancel", @@ -939,6 +1334,7 @@ def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, s headers=csrf, ) report(f"POST /orders/{order2_id}/cancel", code, body) + check_order_status(s_consumer, order2_id, "cancelled", "second order after cancel") code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders/paid", @@ -950,11 +1346,25 @@ def phase8_order(s_consumer: Session, s_actor: Session, player_id, service_id, s headers=csrf, ) report("POST /orders/paid (create+pay)", code, body) + paid_order = body.get("order", {}) if isinstance(body, dict) else {} + report_check( + "create+pay order starts pending accept", + body.get("ok") is True + and as_int(paid_order.get("id")) > 0 + and paid_order.get("status") == "pending_accept", + body, + ) return order_id -def phase8b_review(s_consumer: Session, order_id, player_user_id): +def phase8b_review( + s_consumer: Session, + s_actor: Session, + order_id, + consumer_user_id, + player_user_id, +): print("\n=== Phase 8b: Reviews ===") if not order_id: skip("Review flow", "No pending_review order id") @@ -983,14 +1393,60 @@ def phase8b_review(s_consumer: Session, order_id, player_user_id): body, ) + code, body, _ = s_actor.post( + f"{GATEWAY}/api/v1/orders/{order_id}/review", + json_body={"rating": 4, "content": "smooth buyer"}, + headers=s_actor.csrf_headers(), + ) + report(f"POST /orders/{order_id}/review (counterparty)", code, body) + + order_body = check_order_status(s_consumer, order_id, "completed", "after two reviews") + report_check("completed order has completedAt", bool(order_body.get("completedAt")), order_body) + + code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders/{order_id}/reviews") + report(f"GET /orders/{order_id}/reviews (after two reviews)", code, body) + reviews = pick_items(body) + report_check( + "two-sided reviews are visible and unsealed", + len(reviews) == 2 + and all(same_id(item.get("orderId"), order_id) for item in reviews) + and all(item.get("sealed") is False for item in reviews) + and bool(find_item(reviews, lambda item: item.get("content") == "great service")) + and bool(find_item(reviews, lambda item: item.get("content") == "smooth buyer")), + body, + ) + code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/reviews?limit=20") report("GET /reviews?limit=20", code, body) + report_check( + "public review list contains unsealed review", + bool( + find_item( + pick_items(body), + lambda item: same_id(item.get("orderId"), order_id) + and item.get("sealed") is False, + ) + ), + body, + ) if player_user_id: code, body, _ = s_consumer.get( f"{GATEWAY}/api/v1/users/{player_user_id}/reviews?limit=20", ) report(f"GET /users/{player_user_id}/reviews?limit=20", code, body) + report_check( + "player received review is listed", + bool( + find_item( + pick_items(body), + lambda item: same_id(item.get("orderId"), order_id) + and same_id(item.get("fromUserId"), consumer_user_id) + and item.get("content") == "great service", + ) + ), + body, + ) def phase8c_dispute(s_consumer: Session, s_actor: Session, player_id, service_id, shop_id): @@ -999,6 +1455,14 @@ def phase8c_dispute(s_consumer: Session, s_actor: Session, player_id, service_id skip("Dispute flow", "Missing player or service id") return 0 + code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/users/me") + report("GET /users/me (dispute consumer)", code, body) + consumer_user_id = pick_user_id(body) + + code, body, _ = s_actor.get(f"{GATEWAY}/api/v1/users/me") + report("GET /users/me (dispute actor)", code, body) + actor_user_id = pick_user_id(body) + csrf = s_consumer.csrf_headers() payload = { "playerId": player_id, @@ -1017,6 +1481,11 @@ def phase8c_dispute(s_consumer: Session, s_actor: Session, player_id, service_id report("POST /orders (create for dispute)", code, body) order_obj = body.get("order", {}) if isinstance(body, dict) else {} order_id = order_obj.get("id", 0) if isinstance(order_obj, dict) else 0 + report_check( + "dispute order starts pending payment", + as_int(order_id) > 0 and order_obj.get("status") == "pending_payment", + body, + ) if not order_id: skip("Dispute flow", "No order id returned") return 0 @@ -1027,6 +1496,7 @@ def phase8c_dispute(s_consumer: Session, s_actor: Session, player_id, service_id headers=csrf, ) report(f"POST /orders/{order_id}/pay (dispute flow)", code, body) + check_order_status(s_consumer, order_id, "pending_accept", "dispute order after pay") code, body, _ = s_actor.post( f"{GATEWAY}/api/v1/orders/{order_id}/accept", @@ -1034,6 +1504,7 @@ def phase8c_dispute(s_consumer: Session, s_actor: Session, player_id, service_id headers=s_actor.csrf_headers(), ) report(f"POST /orders/{order_id}/accept (dispute flow)", code, body) + check_order_status(s_consumer, order_id, "in_progress", "dispute order after accept") code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/orders/{order_id}/dispute", @@ -1044,16 +1515,44 @@ def phase8c_dispute(s_consumer: Session, s_actor: Session, player_id, service_id headers=csrf, ) report(f"POST /orders/{order_id}/dispute", code, body) + check_order_status(s_consumer, order_id, "disputed", "after dispute create") code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders/{order_id}/dispute") report(f"GET /orders/{order_id}/dispute", code, body) dispute_id = as_int(body.get("id")) if isinstance(body, dict) else 0 + report_check( + "created dispute fields", + dispute_id > 0 + and same_id(body.get("orderId"), order_id) + and same_id(body.get("initiatorId"), consumer_user_id) + and same_id(body.get("respondentId"), actor_user_id) + and body.get("reason") == "test dispute reason" + and body.get("evidence") == ["http://example.com/evidence.jpg"] + and body.get("status") == "open", + body, + ) code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/disputes") report("GET /disputes", code, body) + report_check( + "dispute list contains created dispute", + bool(find_item_by_id(pick_items(body), dispute_id)), + body, + ) code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/disputes?status=open") report("GET /disputes?status=open", code, body) + report_check( + "open dispute list contains created open dispute", + bool( + find_item( + pick_items(body), + lambda item: same_id(item.get("id"), dispute_id) + and item.get("status") == "open", + ) + ), + body, + ) if dispute_id: code, body, _ = s_actor.post( @@ -1069,6 +1568,15 @@ def phase8c_dispute(s_consumer: Session, s_actor: Session, player_id, service_id code, body, ) + code, body, _ = s_consumer.get(f"{GATEWAY}/api/v1/orders/{order_id}/dispute") + report(f"GET /orders/{order_id}/dispute (after response)", code, body) + report_check( + "responded dispute fields", + body.get("status") == "reviewing" + and body.get("respondentReason") == "test respondent response" + and body.get("respondentEvidence") == ["http://example.com/response.jpg"], + body, + ) code, body, _ = s_consumer.post( f"{GATEWAY}/api/v1/disputes/{dispute_id}/appeal", @@ -1089,6 +1597,11 @@ def phase8d_notifications(s: Session): print("\n=== Phase 8d: Notifications ===") code, body, _ = s.get(f"{GATEWAY}/api/v1/notifications") report("GET /notifications", code, body) + report_check( + "GET /notifications returns list shape", + code == 200 and isinstance(pick_items(body), list) and isinstance(body.get("meta"), dict), + body, + ) items = pick_items(body) if code == 200 and isinstance(body, dict) else [] if items: @@ -1100,6 +1613,14 @@ def phase8d_notifications(s: Session): headers=s.csrf_headers(), ) report(f"PUT /notifications/{notification_id}/read", code, body) + code, body, _ = s.get(f"{GATEWAY}/api/v1/notifications") + report("GET /notifications (after single read)", code, body) + notification = find_item_by_id(pick_items(body), notification_id) + report_check( + "single notification is marked read", + bool(notification) and notification.get("read") is True, + body, + ) else: skip("PUT /notifications/:id/read", "No notification item returned") @@ -1109,6 +1630,14 @@ def phase8d_notifications(s: Session): headers=s.csrf_headers(), ) report("PUT /notifications/read-all", code, body) + code, body, _ = s.get(f"{GATEWAY}/api/v1/notifications") + report("GET /notifications (after read-all)", code, body) + items = pick_items(body) + report_check( + "all returned notifications are read", + all(item.get("read") is True for item in items), + body, + ) def phase8e_search_and_favorites(s: Session, user_id, player_id, shop_id): @@ -1134,6 +1663,11 @@ def phase8e_search_and_favorites(s: Session, user_id, player_id, shop_id): code, body, _ = s.get(f"{GATEWAY}/api/v1/favorites") report("GET /favorites", code, body) + report_check( + "GET /favorites returns list shape", + code == 200 and isinstance(pick_items(body), list) and isinstance(body.get("meta"), dict), + body, + ) if not player_id or not user_id: skip("Favorites mutation flow", "Missing user or player id") @@ -1164,8 +1698,34 @@ def phase8e_search_and_favorites(s: Session, user_id, player_id, shop_id): code, body, _ = s.get(f"{GATEWAY}/api/v1/favorites") report("GET /favorites (after add)", code, body) + favorite_items = pick_items(body) + report_check( + "player favorite is listed", + bool( + find_item( + favorite_items, + lambda item: item.get("targetType") == "player" + and same_id(item.get("targetId"), player_id) + and same_id(item.get("userId"), user_id), + ) + ), + body, + ) + if shop_id: + report_check( + "shop favorite is listed", + bool( + find_item( + favorite_items, + lambda item: item.get("targetType") == "shop" + and same_id(item.get("targetId"), shop_id) + and same_id(item.get("userId"), user_id), + ) + ), + body, + ) favorite_id = 0 - for item in pick_items(body): + for item in favorite_items: if ( item.get("targetType") == "player" and str(item.get("targetId")) == str(player_id) @@ -1188,6 +1748,19 @@ def phase8e_search_and_favorites(s: Session, user_id, player_id, shop_id): report(f"GET /users/{user_id}/favorites/check (after delete)", code, body) if code == 200: report_check("favorite check after delete", body.get("favorited") is False, body) + code, body, _ = s.get(f"{GATEWAY}/api/v1/favorites") + report("GET /favorites (after delete)", code, body) + report_check( + "deleted player favorite is absent", + not bool( + find_item( + pick_items(body), + lambda item: item.get("targetType") == "player" + and same_id(item.get("targetId"), player_id), + ) + ), + body, + ) def phase9_wallet(s: Session): @@ -1196,6 +1769,13 @@ def phase9_wallet(s: Session): code, body, _ = s.get(f"{GATEWAY}/api/v1/wallet/balance") report("GET /wallet/balance", code, body) + initial_balance = as_decimal(body.get("balance")) or Decimal("0") + report_check( + "wallet balance response has numeric fields", + as_decimal(body.get("balance")) is not None + and as_decimal(body.get("frozenBalance")) is not None, + body, + ) code, body, _ = s.post( f"{GATEWAY}/api/v1/wallet/topup", @@ -1203,6 +1783,14 @@ def phase9_wallet(s: Session): headers=csrf, ) report("POST /wallet/topup", code, body) + code, body, _ = s.get(f"{GATEWAY}/api/v1/wallet/balance") + report("GET /wallet/balance (after topup)", code, body) + topup_balance = as_decimal(body.get("balance")) + report_check( + "wallet balance increases after topup", + topup_balance == initial_balance + Decimal("100"), + body, + ) code, body, _ = s.post( f"{GATEWAY}/api/v1/wallet/withdraw", @@ -1210,9 +1798,38 @@ def phase9_wallet(s: Session): headers=csrf, ) report("POST /wallet/withdraw", code, body) + code, body, _ = s.get(f"{GATEWAY}/api/v1/wallet/balance") + report("GET /wallet/balance (after withdraw)", code, body) + withdraw_balance = as_decimal(body.get("balance")) + report_check( + "wallet balance decreases after withdraw", + topup_balance is not None and withdraw_balance == topup_balance - Decimal("10"), + body, + ) code, body, _ = s.get(f"{GATEWAY}/api/v1/wallet/transactions") report("GET /wallet/transactions", code, body) + transactions = pick_items(body) + report_check( + "wallet transactions include topup and withdrawal", + bool( + find_item( + transactions, + lambda item: item.get("type") == "topup" + and as_decimal(item.get("amount")) == Decimal("100") + and item.get("description") == "topup via alipay", + ) + ) + and bool( + find_item( + transactions, + lambda item: item.get("type") == "withdrawal" + and as_decimal(item.get("amount")) == Decimal("10") + and item.get("description") == "withdraw via alipay", + ) + ), + body, + ) def phase10_community(s: Session, user_id): @@ -1231,13 +1848,33 @@ def phase10_community(s: Session, user_id): ) report("POST /posts (create)", code, body) post_id = body.get("id", 0) + report_check( + "created post fields", + as_int(post_id) > 0 + and body.get("title") == "Test Post" + and body.get("content") == "Hello world" + and body.get("tags") == ["test"] + and body.get("pinned") is False + and as_int(body.get("likeCount")) == 0 + and as_int(body.get("commentCount")) == 0 + and body.get("liked") is False + and same_id((body.get("author") or {}).get("id"), user_id), + body, + ) code, body, _ = s.get(f"{GATEWAY}/api/v1/posts") report("GET /posts", code, body) + report_check("created post appears in list", bool(find_item_by_id(pick_items(body), post_id)), body) if post_id: code, body, _ = s.get(f"{GATEWAY}/api/v1/posts/{post_id}") report(f"GET /posts/{post_id}", code, body) + report_check( + f"GET /posts/{post_id} returns created post", + same_id(body.get("id"), post_id) and body.get("title") == "Test Post", + body, + ) + original_like_count = as_int(body.get("likeCount")) code, body, _ = s.post( f"{GATEWAY}/api/v1/posts/{post_id}/like", @@ -1245,12 +1882,28 @@ def phase10_community(s: Session, user_id): headers=csrf, ) report(f"POST /posts/{post_id}/like", code, body) + code, body, _ = s.get(f"{GATEWAY}/api/v1/posts/{post_id}") + report(f"GET /posts/{post_id} (after like)", code, body) + report_check( + "post like state is visible", + body.get("liked") is True + and as_int(body.get("likeCount")) >= original_like_count + 1, + 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.get(f"{GATEWAY}/api/v1/posts/{post_id}") + report(f"GET /posts/{post_id} (after unlike)", code, body) + report_check( + "post unlike state is visible", + body.get("liked") is False + and as_int(body.get("likeCount")) == original_like_count, + body, + ) code, body, _ = s.post( f"{GATEWAY}/api/v1/posts/{post_id}/pin", @@ -1258,12 +1911,18 @@ def phase10_community(s: Session, user_id): headers=csrf, ) report(f"POST /posts/{post_id}/pin", code, body) + code, body, _ = s.get(f"{GATEWAY}/api/v1/posts/{post_id}") + report(f"GET /posts/{post_id} (after pin)", code, body) + report_check("post pin state is visible", body.get("pinned") is True, 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.get(f"{GATEWAY}/api/v1/posts/{post_id}") + report(f"GET /posts/{post_id} (after unpin)", code, body) + report_check("post unpin state is visible", body.get("pinned") is False, body) code, body, _ = s.post( f"{GATEWAY}/api/v1/posts/{post_id}/comments", @@ -1272,9 +1931,23 @@ def phase10_community(s: Session, user_id): ) report(f"POST /posts/{post_id}/comments", code, body) comment_id = body.get("id", 0) + report_check( + "created comment fields", + as_int(comment_id) > 0 + and body.get("content") == "Nice post!" + and same_id((body.get("author") or {}).get("id"), user_id) + and as_int(body.get("likeCount")) == 0 + and body.get("liked") is False, + body, + ) code, body, _ = s.get(f"{GATEWAY}/api/v1/posts/{post_id}/comments") report(f"GET /posts/{post_id}/comments", code, body) + report_check( + "created comment appears in list", + bool(find_item_by_id(pick_items(body), comment_id)), + body, + ) if comment_id: code, body, _ = s.post( @@ -1283,15 +1956,36 @@ def phase10_community(s: Session, user_id): headers=csrf, ) report(f"POST /comments/{comment_id}/like", code, body) + code, body, _ = s.get(f"{GATEWAY}/api/v1/posts/{post_id}/comments") + report(f"GET /posts/{post_id}/comments (after comment like)", code, body) + comment = find_item_by_id(pick_items(body), comment_id) + report_check( + "comment like state is visible", + bool(comment) + and comment.get("liked") is True + and as_int(comment.get("likeCount")) >= 1, + 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/posts/{post_id}/comments") + report(f"GET /posts/{post_id}/comments (after comment unlike)", code, body) + comment = find_item_by_id(pick_items(body), comment_id) + report_check( + "comment unlike state is visible", + bool(comment) + and comment.get("liked") is False + and as_int(comment.get("likeCount")) == 0, + body, + ) code, body, _ = s.get(f"{GATEWAY}/api/v1/users/{user_id}/posts") report(f"GET /users/{user_id}/posts", code, body) + report_check("user posts contain created post", bool(find_item_by_id(pick_items(body), post_id)), body) return post_id @@ -1315,7 +2009,7 @@ def phase11_objectstory(s: Session): if code == 200: report_check( "POST /upload returned url", - bool(body.get("url")), + isinstance(body.get("url"), str) and body.get("url", "").startswith("http"), {"url": body.get("url", "")}, ) @@ -1340,6 +2034,11 @@ def phase12_email(s: Session): headers=csrf, ) report("POST /email/verification-code/send (gateway)", code, body) + report_check( + "email verification send returns request id", + code == 200 and bool(body.get("requestId")) and as_int(body.get("expireInSec")) > 0, + body, + ) code, body, _ = s.post( f"{GATEWAY}/api/v1/auth/forgot-password/send", @@ -1347,6 +2046,11 @@ def phase12_email(s: Session): headers=csrf, ) report("POST /auth/forgot-password/send (gateway)", code, body) + report_check( + "forgot password send returns request id", + code == 200 and bool(body.get("requestId")) and as_int(body.get("expireInSec")) > 0, + body, + ) def phase13_logout(s: Session): @@ -1358,6 +2062,8 @@ def phase13_logout(s: Session): headers=csrf, ) report("POST /auth/logout", code, body) + code, body, _ = s.get(f"{GATEWAY}/api/v1/users/me") + report("GET /users/me (after logout)", code, body, expect_status=(401, 403)) def phase14_reset_password(username, email, new_password): @@ -1406,6 +2112,11 @@ def phase14_reset_password(username, email, new_password): headers=s_login.csrf_headers(), ) report("POST /auth/login (after reset)", code, body) + report_check( + "reset password allows login with new password", + code == 200 and bool(s_login.get_cookie("JToken")) and pick_user_id(body) > 0, + {"userId": pick_user_id(body), "hasJToken": bool(s_login.get_cookie("JToken"))}, + ) def phase15_player_service_delete(s: Session, service_id): @@ -1419,6 +2130,13 @@ def phase15_player_service_delete(s: Session, service_id): headers=csrf, ) report(f"DELETE /services/{service_id}", code, body) + code, body, _ = s.get(f"{GATEWAY}/api/v1/services") + report("GET /services (after delete)", code, body) + report_check( + "deleted service is absent from service list", + not bool(find_item_by_id(pick_items(body), service_id)), + body, + ) def main(): @@ -1486,7 +2204,7 @@ def main(): shop_id = phase7_shop(s_user, s_consumer, user_id, invited_player_id) order_id = phase8_order(s_consumer, s_user, player_id, service_id, shop_id) - phase8b_review(s_consumer, order_id, user_id) + phase8b_review(s_consumer, s_user, order_id, consumer_user_id, user_id) phase8c_dispute(s_consumer, s_user, player_id, service_id, shop_id) phase8d_notifications(s_consumer) phase8e_search_and_favorites(s_consumer, consumer_user_id, player_id, shop_id)