test_api_batch3.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  1. # -*- coding: utf-8 -*-
  2. """客户端 api 第 3 批接口冒烟:用户中心 / 我的优惠券 / 余额积分明细 / 售后 / 充值 / 上传。
  3. 用法:起 Java 18080 后执行
  4. python tools/test_api_batch3.py
  5. 环境变量:YOSHOP_API_BASE(默认 http://127.0.0.1:18080)
  6. 说明:
  7. - 全部是需登录接口,脚本会往 Redis 写测试 token(yoshop:api:token:apitest-batch3)与短信验证码;
  8. - 会临时造订单、售后单、充值订单、支付方式、优惠券领取记录、上传文件、临时用户,
  9. finally 中全部清理并把用户资料(手机号 / 昵称 / 头像)恢复原值。
  10. """
  11. import json
  12. import os
  13. import re
  14. import socket
  15. import sys
  16. import pymysql
  17. import requests
  18. from _root import ROOT
  19. from redis_probe import cmd
  20. BASE = os.environ.get("YOSHOP_API_BASE", "http://127.0.0.1:18080")
  21. TOKEN = "apitest-batch3"
  22. USER_ID = 10001
  23. GOODS_ID = 10009
  24. SKU_ID = "10009"
  25. # 领取测试用券(满 100 减 10、领取后 7 天有效)
  26. COUPON_ID = 10001
  27. PLAN_ID = 10001
  28. EXPRESS_ID = 10007
  29. YML = str(ROOT / "yoshop-java/src/main/resources/application.yml")
  30. TEXT = open(YML, encoding="utf-8").read()
  31. DB_PWD = re.findall(r"password:\s*(.+)", TEXT)[0].strip().strip("\"'")
  32. REDIS_PWD = re.findall(r"password:\s*(.+)", TEXT)[1].strip().strip("\"'")
  33. S = requests.Session()
  34. S.trust_env = False
  35. passed = 0
  36. failed = 0
  37. created_orders = []
  38. created_refunds = []
  39. created_recharge_orders = []
  40. created_files = []
  41. temp_user_id = None
  42. user_backup = {}
  43. coupon_receive_backup = 0
  44. # 1x1 PNG
  45. PNG = bytes.fromhex(
  46. "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
  47. "0000000d4944415408d76368f8ffff3f0005fe02fea735e1f90000000049454e44ae426082"
  48. )
  49. def check(name, cond, detail=""):
  50. global passed, failed
  51. if cond:
  52. passed += 1
  53. print(f" PASS {name}")
  54. else:
  55. failed += 1
  56. print(f" FAIL {name} {detail}")
  57. def call(method, path, params=None, body=None, token=TOKEN, files=None, data=None):
  58. headers = {"platform": "MP-WEIXIN"}
  59. if token:
  60. headers["Access-Token"] = token
  61. if files is not None:
  62. resp = S.post(BASE + path, params=params or {}, files=files, data=data or {},
  63. headers=headers, timeout=60)
  64. elif method == "GET":
  65. resp = S.get(BASE + path, params=params or {}, headers=headers, timeout=60)
  66. else:
  67. resp = S.post(BASE + path, params=params or {}, json=body, headers=headers, timeout=60)
  68. try:
  69. return resp.json()
  70. except Exception:
  71. return {"status": resp.status_code, "raw": resp.text[:300]}
  72. def get(path, params=None, token=TOKEN):
  73. return call("GET", path, params=params, token=token)
  74. def post(path, body=None, params=None, token=TOKEN):
  75. return call("POST", path, params=params, body=body, token=token)
  76. def upload(path, token=TOKEN, check_login=1, name="test.png"):
  77. files = {"file": (name, PNG, "image/png")}
  78. return call("POST", path, token=token, files=files, data={"checkLogin": str(check_login)})
  79. def snake_keys(obj, prefix="$"):
  80. found = []
  81. if isinstance(obj, dict):
  82. for k, v in obj.items():
  83. if isinstance(k, str) and re.fullmatch(r"[a-z0-9]+(_[a-z0-9]+)+", k):
  84. found.append(f"{prefix}.{k}")
  85. found.extend(snake_keys(v, f"{prefix}.{k}"))
  86. elif isinstance(obj, list):
  87. for i, v in enumerate(obj[:3]):
  88. found.extend(snake_keys(v, f"{prefix}[{i}]"))
  89. return found
  90. def db(sql):
  91. conn = pymysql.connect(host="192.168.1.177", port=3306, user="root", password=DB_PWD,
  92. database="shop", charset="utf8mb4", autocommit=True)
  93. try:
  94. cur = conn.cursor()
  95. cur.execute(sql)
  96. return cur.fetchall()
  97. finally:
  98. conn.close()
  99. def db_exec(sql):
  100. conn = pymysql.connect(host="192.168.1.177", port=3306, user="root", password=DB_PWD,
  101. database="shop", charset="utf8mb4", autocommit=True)
  102. try:
  103. cur = conn.cursor()
  104. cur.execute(sql)
  105. return cur.lastrowid
  106. finally:
  107. conn.close()
  108. def redis_set(key, value):
  109. sock = socket.create_connection(("192.168.1.177", 6379), timeout=5)
  110. try:
  111. cmd(sock, "AUTH", REDIS_PWD)
  112. cmd(sock, "SET", key, str(value))
  113. cmd(sock, "EXPIRE", key, "3600")
  114. finally:
  115. sock.close()
  116. def field(obj, name, default=None):
  117. return obj.get(name, default) if isinstance(obj, dict) else default
  118. def make_order():
  119. """造一条已完成订单 + 订单商品,返回 (order_id, order_goods_id)"""
  120. order_no = "TESTB3" + str(os.getpid()) + str(len(created_orders))
  121. order_id = db_exec(
  122. "INSERT INTO yoshop_order (order_no, order_type, total_price, order_price, coupon_id, coupon_money, "
  123. "points_money, points_num, pay_price, update_price, buyer_remark, pay_method, pay_status, trade_id, "
  124. "delivery_type, express_price, delivery_status, receipt_status, order_status, points_bonus, "
  125. "is_settled, is_comment, order_source, order_source_id, order_source_data, platform, user_id, "
  126. "is_delete, store_id, create_time, update_time) VALUES "
  127. f"('{order_no}', 10, 80.00, 80.00, 0, 0.00, 0.00, 0, 80.00, 0.00, '', '', 20, 0, "
  128. f"10, 0.00, 20, 20, 30, 0, 1, 1, 10, 0, '[]', 'H5', {USER_ID}, 0, 10001, "
  129. "UNIX_TIMESTAMP(), UNIX_TIMESTAMP())")
  130. created_orders.append(order_id)
  131. goods_id = db_exec(
  132. "INSERT INTO yoshop_order_goods (goods_id, goods_type, goods_name, image_id, deduct_stock_type, "
  133. "spec_type, goods_sku_id, goods_props, content, goods_no, goods_price, line_price, goods_weight, "
  134. "is_user_grade, grade_ratio, grade_goods_price, grade_total_money, coupon_money, points_money, "
  135. "points_num, points_bonus, total_num, total_price, total_pay_price, delivery_status, delivery_num, "
  136. "is_comment, order_id, user_id, goods_source_id, store_id, create_time) VALUES "
  137. f"({GOODS_ID}, 10, '正宗荔浦芋头', 0, 10, 20, '{SKU_ID}', '[]', '', '', 80.00, 100.00, 4.5, "
  138. f"0, 0, 0.00, 0.00, 0.00, 0.00, 0, 0, 1, 80.00, 80.00, 20, 1, 1, {order_id}, {USER_ID}, 0, "
  139. "10001, UNIX_TIMESTAMP())")
  140. return order_id, goods_id
  141. def make_payment(client, method, is_default=1):
  142. return db_exec(
  143. "INSERT INTO yoshop_payment (client, method, is_must_template, template_id, is_enable, is_default, "
  144. f"others, store_id, create_time, update_time) VALUES ('{client}', '{method}', 0, 0, 1, {is_default}, "
  145. "'[]', 10001, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())")
  146. def cleanup():
  147. try:
  148. for order_id in created_orders:
  149. db_exec(f"DELETE FROM yoshop_order_goods WHERE order_id={order_id}")
  150. db_exec(f"DELETE FROM yoshop_order WHERE order_id={order_id}")
  151. for refund_id in created_refunds:
  152. db_exec(f"DELETE FROM yoshop_order_refund_image WHERE order_refund_id={refund_id}")
  153. db_exec(f"DELETE FROM yoshop_order_refund_address WHERE order_refund_id={refund_id}")
  154. db_exec(f"DELETE FROM yoshop_order_refund WHERE order_refund_id={refund_id}")
  155. for order_id in created_recharge_orders:
  156. db_exec(f"DELETE FROM yoshop_recharge_order_plan WHERE order_id={order_id}")
  157. db_exec(f"DELETE FROM yoshop_recharge_order WHERE order_id={order_id}")
  158. db_exec("DELETE FROM yoshop_payment WHERE store_id=10001 AND method IN ('wechat','balance') "
  159. "AND client='MP-WEIXIN'")
  160. db_exec(f"DELETE FROM yoshop_user_coupon WHERE user_id={USER_ID} AND coupon_id={COUPON_ID}")
  161. db_exec(f"UPDATE yoshop_coupon SET receive_num={coupon_receive_backup} WHERE coupon_id={COUPON_ID}")
  162. if temp_user_id:
  163. db_exec(f"DELETE FROM yoshop_user WHERE user_id={temp_user_id}")
  164. if user_backup:
  165. db_exec("UPDATE yoshop_user SET mobile='{}', nick_name='{}', avatar_id={} WHERE user_id={}".format(
  166. user_backup["mobile"], user_backup["nick_name"], user_backup["avatar_id"], USER_ID))
  167. for file_id in created_files:
  168. db_exec(f"DELETE FROM yoshop_upload_file WHERE file_id={file_id}")
  169. print(" CLEAN 测试数据已清理")
  170. except Exception as e: # noqa: BLE001
  171. print(" CLEAN 失败:", e)
  172. def main():
  173. global temp_user_id, coupon_receive_backup
  174. print("== 客户端 api 第 3 批(用户中心 / 优惠券 / 明细 / 售后 / 充值 / 上传) ==")
  175. redis_set("yoshop:api:token:" + TOKEN, USER_ID)
  176. row = db(f"SELECT mobile, nick_name, avatar_id FROM yoshop_user WHERE user_id={USER_ID}")[0]
  177. user_backup.update(mobile=row[0], nick_name=row[1], avatar_id=row[2])
  178. coupon_receive_backup = db(f"SELECT receive_num FROM yoshop_coupon WHERE coupon_id={COUPON_ID}")[0][0]
  179. # ---------------------------------------------------------------- 未登录
  180. print("-- 未登录校验")
  181. for path, label in [("/api/user/info", "user/info"), ("/api/user/assets", "user/assets"),
  182. ("/api/myCoupon/list", "myCoupon/list"), ("/api/refund/list", "refund/list")]:
  183. r = get(path, token=None)
  184. check(f"未登录 {label} 返回 401", r.get("status") == 401, r.get("message"))
  185. # ---------------------------------------------------------------- 用户信息
  186. print("-- 用户中心")
  187. r = get("/api/user/info")
  188. info = field(r.get("data"), "userInfo") or {}
  189. check("user/info status=200", r.get("status") == 200, r.get("message"))
  190. check("user/info 手机号打码", info.get("mobile") == "185****1340", info.get("mobile"))
  191. check("user/info 含 avatarUrl / grade / balance 键",
  192. all(k in info for k in ("avatarUrl", "grade", "balance", "points")), list(info.keys()))
  193. check("user/info 隐藏 isDelete/storeId/createTime",
  194. not any(k in info for k in ("isDelete", "storeId", "createTime", "updateTime", "password")),
  195. [k for k in ("isDelete", "storeId", "createTime") if k in info])
  196. check("user/info 无 snake_case", not snake_keys(info), snake_keys(info)[:3])
  197. r = get("/api/user/assets")
  198. assets = field(r.get("data"), "assets") or {}
  199. check("user/assets 三个字段齐全", set(assets.keys()) == {"balance", "points", "coupon"}, assets.keys())
  200. check("user/assets balance 两位小数字符串",
  201. isinstance(assets.get("balance"), str) and re.fullmatch(r"-?\d+\.\d{2}", assets.get("balance", "")),
  202. assets.get("balance"))
  203. check("user/assets 无 snake_case", not snake_keys(assets), snake_keys(assets)[:3])
  204. # ---------------------------------------------------------------- 明细
  205. print("-- 余额 / 积分明细")
  206. r = get("/api/balance.log/list")
  207. logs = field(r.get("data"), "data") or []
  208. check("balance.log/list status=200", r.get("status") == 200, r.get("message"))
  209. check("balance.log/list 有数据且按时间倒序",
  210. len(logs) > 0 and (len(logs) < 2 or logs[0]["createTime"] >= logs[1]["createTime"]), len(logs))
  211. check("balance.log/list 隐藏 storeId 与 user",
  212. all("storeId" not in x and "user" not in x for x in logs[:3]), logs[:1])
  213. check("balance.log/list perPage=15", field(r.get("data"), "perPage") == 15, field(r.get("data"), "perPage"))
  214. r = get("/api/points.log/list")
  215. plogs = field(r.get("data"), "data") or []
  216. check("points.log/list status=200", r.get("status") == 200, r.get("message"))
  217. check("points.log/list 字段为 logId/value/describe",
  218. all({"logId", "value", "describe"} <= set(x.keys()) for x in plogs[:3]), plogs[:1])
  219. check("points.log/list 无 snake_case", not snake_keys(plogs), snake_keys(plogs)[:3])
  220. # ---------------------------------------------------------------- 修改资料
  221. print("-- 修改资料")
  222. r = post("/api/user/personal", {"form": {"nickName": "接口自测昵称"}})
  223. check("user/personal 修改成功", r.get("status") == 200 and r.get("message") == "恭喜您,信息修改成功", r)
  224. nick = db(f"SELECT nick_name, avatar_id FROM yoshop_user WHERE user_id={USER_ID}")[0]
  225. check("user/personal 昵称落库", nick[0] == "接口自测昵称", nick)
  226. check("user/personal 头像沿用原值", nick[1] == user_backup["avatar_id"], nick[1])
  227. # ---------------------------------------------------------------- 绑定手机号
  228. print("-- 绑定手机号")
  229. new_mobile = "19900002222"
  230. r = post("/api/user/bindMobile", {"form": {"mobile": new_mobile, "smsCode": "123456"}})
  231. check("bindMobile 无验证码时报错", r.get("status") == 500 and "短信验证码不存在" in (r.get("message") or ""), r)
  232. redis_set("yoshop:api:sms:" + new_mobile, json.dumps({"code": "654321", "times": 5}))
  233. r = post("/api/user/bindMobile", {"form": {"mobile": new_mobile, "smsCode": "000000"}})
  234. check("bindMobile 验证码错误时报错", r.get("status") == 500 and "验证码不正确" in (r.get("message") or ""), r)
  235. left = json.loads(db_redis("yoshop:api:sms:" + new_mobile) or "null")
  236. check("bindMobile 错误一次后剩余次数减一", left and left.get("times") == 4, left)
  237. r = post("/api/user/bindMobile", {"form": {"mobile": new_mobile, "smsCode": "654321"}})
  238. check("bindMobile 绑定成功", r.get("status") == 200 and r.get("message") == "恭喜您,手机号绑定成功", r)
  239. check("bindMobile 手机号落库", db(f"SELECT mobile FROM yoshop_user WHERE user_id={USER_ID}")[0][0] == new_mobile)
  240. check("bindMobile 验证码用后即焚", db_redis("yoshop:api:sms:" + new_mobile) is None)
  241. # 手机号已被占用
  242. temp_user_id = db_exec(
  243. "INSERT INTO yoshop_user (mobile, nick_name, avatar_id, gender, country, province, city, address_id, "
  244. "balance, points, pay_money, expend_money, grade_id, platform, last_login_time, is_delete, store_id, "
  245. "create_time, update_time) VALUES ('19900003333', '临时用户', 0, 0, '', '', '', 0, 0.00, 0, 0.00, "
  246. "0.00, 0, 'H5', 0, 0, 10001, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())")
  247. redis_set("yoshop:api:sms:19900003333", json.dumps({"code": "111111", "times": 5}))
  248. r = post("/api/user/bindMobile", {"form": {"mobile": "19900003333", "smsCode": "111111"}})
  249. check("bindMobile 手机号已占用时报错",
  250. r.get("status") == 500 and "已绑定其他账户" in (r.get("message") or ""), r)
  251. # ---------------------------------------------------------------- 我的优惠券
  252. print("-- 我的优惠券")
  253. r = post("/api/myCoupon/receive", {"couponId": 999999})
  254. check("myCoupon/receive 券不存在时报错",
  255. r.get("status") == 500 and "优惠券不存在" in (r.get("message") or ""), r)
  256. r = post("/api/myCoupon/receive", {"couponId": COUPON_ID})
  257. check("myCoupon/receive 领取成功", r.get("status") == 200 and r.get("message") == "领取成功", r)
  258. recv = db(f"SELECT user_coupon_id, name, is_use, is_expire, start_time, end_time "
  259. f"FROM yoshop_user_coupon WHERE user_id={USER_ID} AND coupon_id={COUPON_ID}")
  260. check("领取记录落库且未使用未过期", len(recv) == 1 and recv[0][2] == 0 and recv[0][3] == 0, recv)
  261. check("领取后有效期为 7 天", len(recv) == 1 and recv[0][5] - recv[0][4] == 7 * 86400, recv)
  262. check("券已领取数量 +1",
  263. db(f"SELECT receive_num FROM yoshop_coupon WHERE coupon_id={COUPON_ID}")[0][0]
  264. == coupon_receive_backup + 1)
  265. r = post("/api/myCoupon/receive", {"couponId": COUPON_ID})
  266. check("myCoupon/receive 重复领取被拒",
  267. r.get("status") == 500 and "已领取该优惠券" in (r.get("message") or ""), r)
  268. r = get("/api/myCoupon/list")
  269. data = field(r.get("data"), "data") or []
  270. check("myCoupon/list 含刚领取的券", any(x.get("couponId") == COUPON_ID for x in data), len(data))
  271. check("myCoupon/list 隐藏 storeId/createTime/updateTime",
  272. all(not {"storeId", "createTime", "updateTime"} & set(x.keys()) for x in data), data[:1])
  273. check("myCoupon/list state 为正常", data and field(data[0].get("state"), "text") == "正常", data[:1])
  274. check("myCoupon/list discount 为 0-10 小数", all(isinstance(x.get("discount"), (int, float)) for x in data),
  275. [x.get("discount") for x in data[:2]])
  276. r = get("/api/myCoupon/list", {"dataType": "isUsable"})
  277. usable = field(r.get("data"), "data") or []
  278. check("myCoupon/list isUsable 含该券", any(x.get("couponId") == COUPON_ID for x in usable), len(usable))
  279. r = get("/api/myCoupon/list", {"dataType": "isUsable", "amount": 50})
  280. filtered = field(r.get("data"), "data") or []
  281. check("myCoupon/list amount=50 过滤掉满 100 的券",
  282. not any(x.get("couponId") == COUPON_ID for x in filtered), len(filtered))
  283. r = get("/api/user/assets")
  284. check("user/assets 可用券数量 +1", field(field(r.get("data"), "assets"), "coupon") == 1,
  285. field(field(r.get("data"), "assets"), "coupon"))
  286. # ---------------------------------------------------------------- 售后
  287. print("-- 售后服务")
  288. order_id, order_goods_id = make_order()
  289. r = get("/api/refund/goods", {"orderGoodsId": order_goods_id})
  290. goods = field(r.get("data"), "goods") or {}
  291. check("refund/goods status=200", r.get("status") == 200, r.get("message"))
  292. check("refund/goods 含商品图与金额", "goodsImage" in goods and goods.get("totalPayPrice") == "80.00",
  293. {k: goods.get(k) for k in ("goodsImage", "totalPayPrice")})
  294. check("refund/goods 无 snake_case", not snake_keys(goods), snake_keys(goods)[:3])
  295. r = get("/api/refund/goods", {"orderGoodsId": 99999999})
  296. check("refund/goods 不存在时报错", r.get("status") == 500 and "未找到订单商品" in (r.get("message") or ""), r)
  297. r = post("/api/refund/apply", {"orderGoodsId": order_goods_id, "form": {"content": "不想要了"}})
  298. check("refund/apply 缺少类型时报错", r.get("status") == 500 and "请选择售后类型" in (r.get("message") or ""), r)
  299. r = post("/api/refund/apply", {"orderGoodsId": order_goods_id,
  300. "form": {"type": 10, "content": "接口自测:商品有瑕疵"}})
  301. check("refund/apply 提交成功", r.get("status") == 200 and r.get("message") == "提交成功", r)
  302. refund_rows = db(f"SELECT order_refund_id, type, apply_desc, audit_status, status, is_user_send "
  303. f"FROM yoshop_order_refund WHERE order_goods_id={order_goods_id}")
  304. check("售后单落库为待审核", len(refund_rows) == 1 and refund_rows[0][3] == 0 and refund_rows[0][4] == 0,
  305. refund_rows)
  306. refund_id = refund_rows[0][0]
  307. created_refunds.append(refund_id)
  308. r = get("/api/refund/goods", {"orderGoodsId": order_goods_id})
  309. check("已申请售后不可重复申请", r.get("status") == 500 and "已申请售后" in (r.get("message") or ""), r)
  310. r = get("/api/refund/detail", {"orderRefundId": refund_id})
  311. detail = field(r.get("data"), "detail") or {}
  312. check("refund/detail stateText=等待审核中", detail.get("stateText") == "等待审核中", detail.get("stateText"))
  313. check("refund/detail 含 orderGoods 且图片为空数组",
  314. isinstance(detail.get("orderGoods"), dict) and detail.get("images") == [], detail.get("images"))
  315. check("refund/detail 隐藏 storeId/updateTime",
  316. not {"storeId", "updateTime"} & set(detail.keys()), [k for k in ("storeId", "updateTime") if k in detail])
  317. check("refund/detail 无 snake_case", not snake_keys(detail), snake_keys(detail)[:3])
  318. r = get("/api/refund/list", {"state": 0})
  319. refund_list = field(r.get("data"), "data") or []
  320. check("refund/list 含该售后单", any(x.get("orderRefundId") == refund_id for x in refund_list), len(refund_list))
  321. check("refund/list 每项均带 stateText", all(x.get("stateText") for x in refund_list), refund_list[:1])
  322. check("refund/list 隐藏 storeId/updateTime/images",
  323. all(not {"storeId", "updateTime", "images"} & set(x.keys()) for x in refund_list), refund_list[:1])
  324. r = get("/api/refund/list", {"state": 20})
  325. check("refund/list state=20 不含进行中的单",
  326. not any(x.get("orderRefundId") == refund_id for x in (field(r.get("data"), "data") or [])), r.get("data"))
  327. # 未审核不允许发货
  328. r = post("/api/refund/delivery", {"orderRefundId": refund_id,
  329. "form": {"expressId": EXPRESS_ID, "expressNo": "SF0001"}})
  330. check("refund/delivery 未审核时报错", r.get("status") == 500 and "不合法" in (r.get("message") or ""), r)
  331. db_exec(f"UPDATE yoshop_order_refund SET audit_status=10 WHERE order_refund_id={refund_id}")
  332. r = post("/api/refund/delivery", {"orderRefundId": refund_id, "form": {"expressNo": "SF0001"}})
  333. check("refund/delivery 未选物流公司时报错",
  334. r.get("status") == 500 and "请选择物流公司" in (r.get("message") or ""), r)
  335. r = post("/api/refund/delivery", {"orderRefundId": refund_id, "form": {"expressId": EXPRESS_ID}})
  336. check("refund/delivery 未填单号时报错",
  337. r.get("status") == 500 and "请填写物流单号" in (r.get("message") or ""), r)
  338. r = post("/api/refund/delivery", {"orderRefundId": refund_id,
  339. "form": {"expressId": EXPRESS_ID, "expressNo": "SF0001"}})
  340. check("refund/delivery 发货成功", r.get("status") == 200 and r.get("message") == "操作成功", r)
  341. sent = db(f"SELECT is_user_send, express_id, express_no, send_time FROM yoshop_order_refund "
  342. f"WHERE order_refund_id={refund_id}")[0]
  343. check("售后单发货信息落库",
  344. sent[0] == 1 and str(sent[1]) == str(EXPRESS_ID) and sent[2] == "SF0001" and sent[3] > 0, sent)
  345. r = get("/api/refund/detail", {"orderRefundId": refund_id})
  346. detail = field(r.get("data"), "detail") or {}
  347. check("refund/detail 发货后 stateText 变化",
  348. detail.get("stateText") == "已发货,待平台确认", detail.get("stateText"))
  349. check("refund/detail 带物流公司名称",
  350. field(detail.get("express"), "expressName") == "中通快递", detail.get("express"))
  351. r = get("/api/refund/detail", {"orderRefundId": 99999999})
  352. check("refund/detail 不存在时报错", r.get("status") == 500 and "未找到该售后单" in (r.get("message") or ""), r)
  353. # ---------------------------------------------------------------- 充值
  354. print("-- 余额充值")
  355. r = get("/api/recharge/center", {"client": "MP-WEIXIN"})
  356. check("recharge/center 无支付方式时报错",
  357. r.get("status") == 500 and "没有可用的支付方式" in (r.get("message") or ""), r)
  358. make_payment("MP-WEIXIN", "wechat", 1)
  359. make_payment("MP-WEIXIN", "balance", 0)
  360. r = get("/api/recharge/center", {"client": "MP-WEIXIN"})
  361. center = r.get("data") or {}
  362. setting = field(center, "setting") or {}
  363. check("recharge/center status=200", r.get("status") == 200, r.get("message"))
  364. check("recharge/center 设置默认值(isCustom=1 / lowestMoney=10 / isMatchPlan=1)",
  365. setting.get("isCustom") == 1 and float(setting.get("lowestMoney")) == 10.0
  366. and setting.get("isMatchPlan") == 1, setting)
  367. check("recharge/center personal 为当前用户",
  368. field(field(center, "personal"), "userId") == USER_ID, field(center, "personal"))
  369. check("recharge/center planList 含套餐", any(p.get("planId") == PLAN_ID for p in
  370. (field(center, "planList") or [])), field(center, "planList"))
  371. check("recharge/center paymentMethods 排除余额支付",
  372. [m.get("method") for m in (field(center, "paymentMethods") or [])] == ["wechat"],
  373. field(center, "paymentMethods"))
  374. check("recharge/center 无 snake_case", not snake_keys(center), snake_keys(center)[:3])
  375. r = post("/api/recharge/submit", {"form": {"method": "balance", "client": "MP-WEIXIN"}})
  376. check("recharge/submit 缺套餐与金额时报错",
  377. r.get("status") == 500 and "请选择充值套餐或输入充值金额" in (r.get("message") or ""), r)
  378. r = post("/api/recharge/submit", {"form": {"method": "balance", "client": "MP-WEIXIN",
  379. "customMoney": "5"}})
  380. check("recharge/submit 低于最低金额时报错",
  381. r.get("status") == 500 and "最低充值金额不能低于10元" in (r.get("message") or ""), r)
  382. r = post("/api/recharge/submit", {"form": {"method": "balance", "client": "MP-WEIXIN",
  383. "customMoney": "100"}})
  384. check("recharge/submit 自定义金额下单成功",
  385. r.get("status") == 200 and r.get("message") == "下单成功" and field(r.get("data"), "payment") == [], r)
  386. row = db(f"SELECT order_id, order_no, recharge_type, plan_id, pay_price, gift_money, actual_money, "
  387. f"pay_status, platform FROM yoshop_recharge_order WHERE user_id={USER_ID} ORDER BY order_id DESC "
  388. "LIMIT 1")[0]
  389. created_recharge_orders.append(row[0])
  390. check("充值订单号以 RC 开头且 18 位以上", str(row[1]).startswith("RC") and len(str(row[1])) >= 18, row[1])
  391. check("自定义充值匹配套餐(赠 10)",
  392. row[2] == 10 and row[3] == PLAN_ID and float(row[5]) == 10.0 and float(row[6]) == 110.0, row)
  393. check("充值订单平台取请求头", row[8] == "MP-WEIXIN", row[8])
  394. snapshot = db(f"SELECT plan_id, plan_name, money, gift_money FROM yoshop_recharge_order_plan "
  395. f"WHERE order_id={row[0]}")
  396. check("套餐快照落库", len(snapshot) == 1 and snapshot[0][0] == PLAN_ID, snapshot)
  397. r = post("/api/recharge/submit", {"form": {"method": "balance", "client": "MP-WEIXIN",
  398. "planId": PLAN_ID}})
  399. check("recharge/submit 指定套餐下单成功", r.get("status") == 200, r)
  400. row2 = db(f"SELECT order_id, recharge_type, plan_id, pay_price, gift_money, actual_money "
  401. f"FROM yoshop_recharge_order WHERE user_id={USER_ID} ORDER BY order_id DESC LIMIT 1")[0]
  402. created_recharge_orders.append(row2[0])
  403. check("套餐充值金额与赠送正确",
  404. row2[1] == 20 and float(row2[3]) == 100.0 and float(row2[4]) == 10.0 and float(row2[5]) == 110.0, row2)
  405. r = post("/api/recharge/submit", {"form": {"method": "wechat", "client": "MP-WEIXIN",
  406. "planId": PLAN_ID}})
  407. check("recharge/submit 微信支付未接入时明确报错",
  408. r.get("status") == 500 and "第三方支付" in (r.get("message") or ""), r)
  409. r = get("/api/recharge/tradeQuery", {"outTradeNo": "RC123456", "method": "wechat",
  410. "client": "MP-WEIXIN"})
  411. check("recharge/tradeQuery 未支付返回 isPay=false",
  412. r.get("status") == 200 and field(r.get("data"), "isPay") is False, r)
  413. check("recharge/tradeQuery 提示未支付",
  414. "订单未支付" in (r.get("message") or ""), r.get("message"))
  415. db_exec(f"UPDATE yoshop_recharge_order SET pay_status=20, pay_time=UNIX_TIMESTAMP(), pay_method='balance' "
  416. f"WHERE order_id={row[0]}")
  417. r = get("/api/recharge.order/list")
  418. orders = field(r.get("data"), "data") or []
  419. check("recharge.order/list 只列已支付订单",
  420. [o.get("orderId") for o in orders] == [row[0]], [o.get("orderId") for o in orders])
  421. check("recharge.order/list 隐藏 tradeId/createTime/storeId",
  422. all(not {"tradeId", "createTime", "storeId"} & set(o.keys()) for o in orders), orders[:1])
  423. check("recharge.order/list 金额为两位小数字符串",
  424. orders and orders[0].get("actualMoney") == "110.00", orders[:1])
  425. check("recharge.order/list 无 snake_case", not snake_keys(orders), snake_keys(orders)[:3])
  426. # ---------------------------------------------------------------- 上传
  427. print("-- 文件上传")
  428. r = upload("/api/upload/image", token=None, check_login=1)
  429. check("upload/image 需要登录时未登录返回 401", r.get("status") == 401, r)
  430. r = upload("/api/upload/image", check_login=1)
  431. file_info = field(r.get("data"), "fileInfo") or {}
  432. check("upload/image 上传成功", r.get("status") == 200 and r.get("message") == "文件上传成功", r)
  433. check("upload/image 返回 fileId 与 previewUrl",
  434. isinstance(file_info.get("fileId"), int) and str(file_info.get("previewUrl", "")).startswith("http"),
  435. file_info)
  436. check("upload/image 隐藏 filePath/fileName/storeId",
  437. not {"filePath", "fileName", "storeId", "uploaderId"} & set(file_info.keys()), list(file_info.keys()))
  438. if file_info.get("fileId"):
  439. created_files.append(file_info["fileId"])
  440. uploader = db(f"SELECT channel, uploader_id, file_type, is_delete FROM yoshop_upload_file "
  441. f"WHERE file_id={file_info['fileId']}")[0]
  442. check("上传记录 channel=20 且记录上传者", uploader[0] == 20 and uploader[1] == USER_ID, uploader)
  443. r = upload("/api/upload/image", token=None, check_login=0)
  444. anon_info = field(r.get("data"), "fileInfo") or {}
  445. check("upload/image checkLogin=0 允许匿名上传", r.get("status") == 200, r)
  446. if anon_info.get("fileId"):
  447. created_files.append(anon_info["fileId"])
  448. check("匿名上传 uploader_id=0",
  449. db(f"SELECT uploader_id FROM yoshop_upload_file WHERE file_id={anon_info['fileId']}")[0][0] == 0)
  450. # 用上传的文件改头像
  451. if file_info.get("fileId"):
  452. r = post("/api/user/personal", {"form": {"avatarId": file_info["fileId"]}})
  453. check("user/personal 设置头像成功", r.get("status") == 200, r)
  454. r = get("/api/user/info")
  455. avatar_url = field(field(r.get("data"), "userInfo"), "avatarUrl")
  456. check("user/info 头像地址随之上线", isinstance(avatar_url, str) and avatar_url.startswith("http"),
  457. avatar_url)
  458. check("user/info avatar 对象非空", field(field(r.get("data"), "userInfo"), "avatar") is not None)
  459. print(f"\n结果:PASS {passed} / FAIL {failed}")
  460. return 1 if failed else 0
  461. def db_redis(key):
  462. sock = socket.create_connection(("192.168.1.177", 6379), timeout=5)
  463. try:
  464. cmd(sock, "AUTH", REDIS_PWD)
  465. result = cmd(sock, "GET", key)
  466. finally:
  467. sock.close()
  468. return result if isinstance(result, str) else None
  469. if __name__ == "__main__":
  470. try:
  471. code = main()
  472. finally:
  473. cleanup()
  474. sys.exit(code)