from __future__ import annotations import argparse import json import sys import urllib.error import urllib.parse import urllib.request from dataclasses import dataclass from typing import Any DEFAULT_BASE_URL = "http://www.pubuli.com:7788/" DEFAULT_ACTIVITY_ID = "ad-x1234" @dataclass class TestResult: name: str passed: bool status_code: int | None message: str response: Any = None def build_url( base_url: str, path: str, query: dict[str, Any] | None = None, ) -> str: url = f"{base_url.rstrip('/')}/{path.lstrip('/')}" if query: filtered = { key: value for key, value in query.items() if value is not None and value != "" } if filtered: url += "?" + urllib.parse.urlencode(filtered) return url def request_json( *, base_url: str, path: str, method: str = "GET", query: dict[str, Any] | None = None, body: dict[str, Any] | None = None, timeout: int = 15, ) -> tuple[int, Any]: url = build_url( base_url=base_url, path=path, query=query, ) headers = { "Accept": "application/json", "User-Agent": "API-System-Smoke-Test/2.0", } data = None if body is not None: data = json.dumps( body, ensure_ascii=False, ).encode("utf-8") headers["Content-Type"] = "application/json" request = urllib.request.Request( url=url, data=data, headers=headers, method=method.upper(), ) try: with urllib.request.urlopen( request, timeout=timeout, ) as response: status_code = int(response.status) raw_text = response.read().decode( "utf-8", errors="replace", ) except urllib.error.HTTPError as exc: status_code = int(exc.code) raw_text = exc.read().decode( "utf-8", errors="replace", ) try: parsed = ( json.loads(raw_text) if raw_text else None ) except json.JSONDecodeError: parsed = raw_text return status_code, parsed def request_text( *, base_url: str, path: str, timeout: int = 15, ) -> tuple[int, str, str]: url = build_url( base_url=base_url, path=path, ) request = urllib.request.Request( url=url, method="GET", headers={ "Accept": "text/html,application/xhtml+xml", "User-Agent": "API-System-Smoke-Test/2.0", }, ) try: with urllib.request.urlopen( request, timeout=timeout, ) as response: return ( int(response.status), response.geturl(), response.read().decode( "utf-8", errors="replace", ), ) except urllib.error.HTTPError as exc: return ( int(exc.code), exc.geturl(), exc.read().decode( "utf-8", errors="replace", ), ) def add_result( results: list[TestResult], *, name: str, passed: bool, status_code: int | None, message: str, response: Any = None, ) -> None: results.append( TestResult( name=name, passed=passed, status_code=status_code, message=message, response=response, ) ) def ask_username() -> str: """ 互動式要求輸入測試會員名。 使用者直接按 Enter 時會持續要求輸入, 避免傳送空白會員名給 API。 """ while True: username = input( "請輸入要測試的會員名:" ).strip() if username: return username print("會員名不可為空,請重新輸入。") def run_tests( args: argparse.Namespace, ) -> list[TestResult]: results: list[TestResult] = [] # 1. 主域名 try: status, final_url, html = request_text( base_url=args.base_url, path="/", timeout=args.timeout, ) passed = status == 200 add_result( results, name="主域名訪問", passed=passed, status_code=status, message=( f"可訪問,最終地址:{final_url}" if passed else f"主域名返回異常,最終地址:{final_url}" ), ) except Exception as exc: add_result( results, name="主域名訪問", passed=False, status_code=None, message=f"{type(exc).__name__}: {exc}", ) return results # 2. 登入頁 try: status, final_url, html = request_text( base_url=args.base_url, path="/login", timeout=args.timeout, ) passed = ( status == 200 and ( "用戶名" in html or "username" in html.lower() ) ) add_result( results, name="登入頁", passed=passed, status_code=status, message=( f"登入頁正常:{final_url}" if passed else "登入頁內容不符合預期" ), ) except Exception as exc: add_result( results, name="登入頁", passed=False, status_code=None, message=f"{type(exc).__name__}: {exc}", ) # 3. 健康檢查 try: status, data = request_json( base_url=args.base_url, path="/health", timeout=args.timeout, ) passed = ( status == 200 and isinstance(data, dict) and data.get("status") == "ok" ) add_result( results, name="健康檢查", passed=passed, status_code=status, message=( "服務狀態正常" if passed else "健康接口回應不符合預期" ), response=data, ) except Exception as exc: add_result( results, name="健康檢查", passed=False, status_code=None, message=f"{type(exc).__name__}: {exc}", ) # 4. 帳號查詢 account_data: Any = None try: status, account_data = request_json( base_url=args.base_url, path="/platform/get-user-account", query={ "username": args.username, }, timeout=args.timeout, ) passed = ( status == 200 and isinstance(account_data, dict) and account_data.get("is_exist") is True ) add_result( results, name="帳號查詢", passed=passed, status_code=status, message=( f"找到會員,狀態={account_data.get('status')}" if passed else "找不到會員或帳號接口格式錯誤" ), response=account_data, ) except Exception as exc: add_result( results, name="帳號查詢", passed=False, status_code=None, message=f"{type(exc).__name__}: {exc}", ) query_username = args.username if isinstance(account_data, dict): query_username = ( account_data.get("username_clean") or account_data.get("username") or args.username ) # 5. 提款記錄 try: status, data = request_json( base_url=args.base_url, path="/platform/withdraw-orders", query={ "username": query_username, }, timeout=args.timeout, ) passed = ( status == 200 and isinstance(data, list) ) add_result( results, name="提款記錄查詢", passed=passed, status_code=status, message=( f"返回 {len(data)} 筆提款記錄" if passed else "提款接口回應格式錯誤" ), response=( data[:5] if isinstance(data, list) else data ), ) except Exception as exc: add_result( results, name="提款記錄查詢", passed=False, status_code=None, message=f"{type(exc).__name__}: {exc}", ) # 6. 未完成提款 try: status, data = request_json( base_url=args.base_url, path="/platform/withdraw-orders-unfinish", query={ "username": query_username, }, timeout=args.timeout, ) passed = ( status == 200 and isinstance(data, list) ) add_result( results, name="未完成提款查詢", passed=passed, status_code=status, message=( f"返回 {len(data)} 筆未完成提款" if passed else "未完成提款接口回應格式錯誤" ), response=( data[:5] if isinstance(data, list) else data ), ) except Exception as exc: add_result( results, name="未完成提款查詢", passed=False, status_code=None, message=f"{type(exc).__name__}: {exc}", ) # 7. 充值記錄 deposit_query: dict[str, Any] = { "username": query_username, } if args.amount: deposit_query["amount"] = args.amount if args.created_at: deposit_query["created_at"] = args.created_at try: status, data = request_json( base_url=args.base_url, path="/platform/deposit-not-received", query=deposit_query, timeout=args.timeout, ) passed = ( status == 200 and isinstance(data, list) ) add_result( results, name="充值記錄查詢", passed=passed, status_code=status, message=( f"返回 {len(data)} 筆充值記錄" if passed else "充值接口回應格式錯誤" ), response=( data[:5] if isinstance(data, list) else data ), ) except Exception as exc: add_result( results, name="充值記錄查詢", passed=False, status_code=None, message=f"{type(exc).__name__}: {exc}", ) # 8. 活動申請模擬 try: status, data = request_json( base_url=args.base_url, path="/platform/apply-activity", method="POST", body={ "username": query_username, "activity_id": args.activity_id, }, timeout=args.timeout, ) passed = ( status == 200 and isinstance(data, dict) and "activity_found" in data and "qualified" in data ) add_result( results, name="活動申請模擬", passed=passed, status_code=status, message=( "活動模擬接口正常" if passed else "活動接口回應格式錯誤" ), response=data, ) except Exception as exc: add_result( results, name="活動申請模擬", passed=False, status_code=None, message=f"{type(exc).__name__}: {exc}", ) # 9. 餘額查詢模擬 try: status, data = request_json( base_url=args.base_url, path="/platform/fix-balance", method="PUT", body={ "username": query_username, }, timeout=args.timeout, ) passed = ( status == 200 and isinstance(data, dict) and "balance" in data ) add_result( results, name="餘額查詢模擬", passed=passed, status_code=status, message=( f"餘額={data.get('balance')}" if passed else "餘額接口回應格式錯誤" ), response=data, ) except Exception as exc: add_result( results, name="餘額查詢模擬", passed=False, status_code=None, message=f"{type(exc).__name__}: {exc}", ) return results def print_results( results: list[TestResult], verbose: bool, ) -> None: print() print("API接口系統測試結果") print("=" * 76) for index, result in enumerate( results, start=1, ): mark = ( "PASS" if result.passed else "FAIL" ) status_text = ( str(result.status_code) if result.status_code is not None else "-" ) print( f"[{mark}] {index:02d}. " f"{result.name} " f"(HTTP {status_text})" ) print(f" {result.message}") if ( verbose and result.response is not None ): print( json.dumps( result.response, ensure_ascii=False, indent=2, default=str, ) ) passed_count = sum( 1 for result in results if result.passed ) failed_count = ( len(results) - passed_count ) print("-" * 76) print( f"總計:{len(results)}," f"通過:{passed_count}," f"失敗:{failed_count}" ) print( "測試結果:全部通過" if failed_count == 0 else "測試結果:存在失敗項目" ) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( description=( "pubuli.com API接口系統" "可輸入會員名測試腳本" ) ) parser.add_argument( "--base-url", default=DEFAULT_BASE_URL, help=( "主域名,預設:" f"{DEFAULT_BASE_URL}" ), ) parser.add_argument( "--username", default="", help=( "測試會員名。" "未指定時,程式會提示輸入。" ), ) parser.add_argument( "--amount", default="", help="可選:指定充值金額", ) parser.add_argument( "--created-at", default="", help=( "可選:指定充值時間," "格式為 ISO-8601" ), ) parser.add_argument( "--activity-id", default=DEFAULT_ACTIVITY_ID, help="測試活動 ID", ) parser.add_argument( "--timeout", type=int, default=15, help="單個請求超時秒數", ) parser.add_argument( "--verbose", action="store_true", help="顯示完整 JSON 回應", ) args = parser.parse_args() # 沒有透過命令列指定會員名時,進入互動輸入模式。 if not args.username.strip(): args.username = ask_username() else: args.username = args.username.strip() return args def main() -> int: args = parse_args() print() print("pubuli.com API接口系統冒煙測試") print("=" * 76) print(f"主域名:{args.base_url}") print(f"測試會員:{args.username}") print("API Key:目前不使用") print(f"請求超時:{args.timeout} 秒") results = run_tests(args) print_results( results=results, verbose=args.verbose, ) return ( 0 if all( result.passed for result in results ) else 1 ) if __name__ == "__main__": sys.exit(main())