|
|
@@ -0,0 +1,559 @@
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
+"""客户端 api 第 3 批接口冒烟:用户中心 / 我的优惠券 / 余额积分明细 / 售后 / 充值 / 上传。
|
|
|
+
|
|
|
+用法:起 Java 18080 后执行
|
|
|
+ python tools/test_api_batch3.py
|
|
|
+环境变量:YOSHOP_API_BASE(默认 http://127.0.0.1:18080)
|
|
|
+
|
|
|
+说明:
|
|
|
+- 全部是需登录接口,脚本会往 Redis 写测试 token(yoshop:api:token:apitest-batch3)与短信验证码;
|
|
|
+- 会临时造订单、售后单、充值订单、支付方式、优惠券领取记录、上传文件、临时用户,
|
|
|
+ finally 中全部清理并把用户资料(手机号 / 昵称 / 头像)恢复原值。
|
|
|
+"""
|
|
|
+import json
|
|
|
+import os
|
|
|
+import re
|
|
|
+import socket
|
|
|
+import sys
|
|
|
+
|
|
|
+import pymysql
|
|
|
+import requests
|
|
|
+
|
|
|
+from _root import ROOT
|
|
|
+from redis_probe import cmd
|
|
|
+
|
|
|
+BASE = os.environ.get("YOSHOP_API_BASE", "http://127.0.0.1:18080")
|
|
|
+TOKEN = "apitest-batch3"
|
|
|
+USER_ID = 10001
|
|
|
+GOODS_ID = 10009
|
|
|
+SKU_ID = "10009"
|
|
|
+# 领取测试用券(满 100 减 10、领取后 7 天有效)
|
|
|
+COUPON_ID = 10001
|
|
|
+PLAN_ID = 10001
|
|
|
+EXPRESS_ID = 10007
|
|
|
+
|
|
|
+YML = str(ROOT / "yoshop-java/src/main/resources/application.yml")
|
|
|
+TEXT = open(YML, encoding="utf-8").read()
|
|
|
+DB_PWD = re.findall(r"password:\s*(.+)", TEXT)[0].strip().strip("\"'")
|
|
|
+REDIS_PWD = re.findall(r"password:\s*(.+)", TEXT)[1].strip().strip("\"'")
|
|
|
+
|
|
|
+S = requests.Session()
|
|
|
+S.trust_env = False
|
|
|
+
|
|
|
+passed = 0
|
|
|
+failed = 0
|
|
|
+created_orders = []
|
|
|
+created_refunds = []
|
|
|
+created_recharge_orders = []
|
|
|
+created_files = []
|
|
|
+temp_user_id = None
|
|
|
+user_backup = {}
|
|
|
+coupon_receive_backup = 0
|
|
|
+
|
|
|
+# 1x1 PNG
|
|
|
+PNG = bytes.fromhex(
|
|
|
+ "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
|
|
|
+ "0000000d4944415408d76368f8ffff3f0005fe02fea735e1f90000000049454e44ae426082"
|
|
|
+)
|
|
|
+
|
|
|
+
|
|
|
+def check(name, cond, detail=""):
|
|
|
+ global passed, failed
|
|
|
+ if cond:
|
|
|
+ passed += 1
|
|
|
+ print(f" PASS {name}")
|
|
|
+ else:
|
|
|
+ failed += 1
|
|
|
+ print(f" FAIL {name} {detail}")
|
|
|
+
|
|
|
+
|
|
|
+def call(method, path, params=None, body=None, token=TOKEN, files=None, data=None):
|
|
|
+ headers = {"platform": "MP-WEIXIN"}
|
|
|
+ if token:
|
|
|
+ headers["Access-Token"] = token
|
|
|
+ if files is not None:
|
|
|
+ resp = S.post(BASE + path, params=params or {}, files=files, data=data or {},
|
|
|
+ headers=headers, timeout=60)
|
|
|
+ elif method == "GET":
|
|
|
+ resp = S.get(BASE + path, params=params or {}, headers=headers, timeout=60)
|
|
|
+ else:
|
|
|
+ resp = S.post(BASE + path, params=params or {}, json=body, headers=headers, timeout=60)
|
|
|
+ try:
|
|
|
+ return resp.json()
|
|
|
+ except Exception:
|
|
|
+ return {"status": resp.status_code, "raw": resp.text[:300]}
|
|
|
+
|
|
|
+
|
|
|
+def get(path, params=None, token=TOKEN):
|
|
|
+ return call("GET", path, params=params, token=token)
|
|
|
+
|
|
|
+
|
|
|
+def post(path, body=None, params=None, token=TOKEN):
|
|
|
+ return call("POST", path, params=params, body=body, token=token)
|
|
|
+
|
|
|
+
|
|
|
+def upload(path, token=TOKEN, check_login=1, name="test.png"):
|
|
|
+ files = {"file": (name, PNG, "image/png")}
|
|
|
+ return call("POST", path, token=token, files=files, data={"checkLogin": str(check_login)})
|
|
|
+
|
|
|
+
|
|
|
+def snake_keys(obj, prefix="$"):
|
|
|
+ found = []
|
|
|
+ if isinstance(obj, dict):
|
|
|
+ for k, v in obj.items():
|
|
|
+ if isinstance(k, str) and re.fullmatch(r"[a-z0-9]+(_[a-z0-9]+)+", k):
|
|
|
+ found.append(f"{prefix}.{k}")
|
|
|
+ found.extend(snake_keys(v, f"{prefix}.{k}"))
|
|
|
+ elif isinstance(obj, list):
|
|
|
+ for i, v in enumerate(obj[:3]):
|
|
|
+ found.extend(snake_keys(v, f"{prefix}[{i}]"))
|
|
|
+ return found
|
|
|
+
|
|
|
+
|
|
|
+def db(sql):
|
|
|
+ conn = pymysql.connect(host="192.168.1.177", port=3306, user="root", password=DB_PWD,
|
|
|
+ database="shop", charset="utf8mb4", autocommit=True)
|
|
|
+ try:
|
|
|
+ cur = conn.cursor()
|
|
|
+ cur.execute(sql)
|
|
|
+ return cur.fetchall()
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+
|
|
|
+def db_exec(sql):
|
|
|
+ conn = pymysql.connect(host="192.168.1.177", port=3306, user="root", password=DB_PWD,
|
|
|
+ database="shop", charset="utf8mb4", autocommit=True)
|
|
|
+ try:
|
|
|
+ cur = conn.cursor()
|
|
|
+ cur.execute(sql)
|
|
|
+ return cur.lastrowid
|
|
|
+ finally:
|
|
|
+ conn.close()
|
|
|
+
|
|
|
+
|
|
|
+def redis_set(key, value):
|
|
|
+ sock = socket.create_connection(("192.168.1.177", 6379), timeout=5)
|
|
|
+ try:
|
|
|
+ cmd(sock, "AUTH", REDIS_PWD)
|
|
|
+ cmd(sock, "SET", key, str(value))
|
|
|
+ cmd(sock, "EXPIRE", key, "3600")
|
|
|
+ finally:
|
|
|
+ sock.close()
|
|
|
+
|
|
|
+
|
|
|
+def field(obj, name, default=None):
|
|
|
+ return obj.get(name, default) if isinstance(obj, dict) else default
|
|
|
+
|
|
|
+
|
|
|
+def make_order():
|
|
|
+ """造一条已完成订单 + 订单商品,返回 (order_id, order_goods_id)"""
|
|
|
+ order_no = "TESTB3" + str(os.getpid()) + str(len(created_orders))
|
|
|
+ order_id = db_exec(
|
|
|
+ "INSERT INTO yoshop_order (order_no, order_type, total_price, order_price, coupon_id, coupon_money, "
|
|
|
+ "points_money, points_num, pay_price, update_price, buyer_remark, pay_method, pay_status, trade_id, "
|
|
|
+ "delivery_type, express_price, delivery_status, receipt_status, order_status, points_bonus, "
|
|
|
+ "is_settled, is_comment, order_source, order_source_id, order_source_data, platform, user_id, "
|
|
|
+ "is_delete, store_id, create_time, update_time) VALUES "
|
|
|
+ f"('{order_no}', 10, 80.00, 80.00, 0, 0.00, 0.00, 0, 80.00, 0.00, '', '', 20, 0, "
|
|
|
+ f"10, 0.00, 20, 20, 30, 0, 1, 1, 10, 0, '[]', 'H5', {USER_ID}, 0, 10001, "
|
|
|
+ "UNIX_TIMESTAMP(), UNIX_TIMESTAMP())")
|
|
|
+ created_orders.append(order_id)
|
|
|
+ goods_id = db_exec(
|
|
|
+ "INSERT INTO yoshop_order_goods (goods_id, goods_type, goods_name, image_id, deduct_stock_type, "
|
|
|
+ "spec_type, goods_sku_id, goods_props, content, goods_no, goods_price, line_price, goods_weight, "
|
|
|
+ "is_user_grade, grade_ratio, grade_goods_price, grade_total_money, coupon_money, points_money, "
|
|
|
+ "points_num, points_bonus, total_num, total_price, total_pay_price, delivery_status, delivery_num, "
|
|
|
+ "is_comment, order_id, user_id, goods_source_id, store_id, create_time) VALUES "
|
|
|
+ f"({GOODS_ID}, 10, '正宗荔浦芋头', 0, 10, 20, '{SKU_ID}', '[]', '', '', 80.00, 100.00, 4.5, "
|
|
|
+ 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, "
|
|
|
+ "10001, UNIX_TIMESTAMP())")
|
|
|
+ return order_id, goods_id
|
|
|
+
|
|
|
+
|
|
|
+def make_payment(client, method, is_default=1):
|
|
|
+ return db_exec(
|
|
|
+ "INSERT INTO yoshop_payment (client, method, is_must_template, template_id, is_enable, is_default, "
|
|
|
+ f"others, store_id, create_time, update_time) VALUES ('{client}', '{method}', 0, 0, 1, {is_default}, "
|
|
|
+ "'[]', 10001, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())")
|
|
|
+
|
|
|
+
|
|
|
+def cleanup():
|
|
|
+ try:
|
|
|
+ for order_id in created_orders:
|
|
|
+ db_exec(f"DELETE FROM yoshop_order_goods WHERE order_id={order_id}")
|
|
|
+ db_exec(f"DELETE FROM yoshop_order WHERE order_id={order_id}")
|
|
|
+ for refund_id in created_refunds:
|
|
|
+ db_exec(f"DELETE FROM yoshop_order_refund_image WHERE order_refund_id={refund_id}")
|
|
|
+ db_exec(f"DELETE FROM yoshop_order_refund_address WHERE order_refund_id={refund_id}")
|
|
|
+ db_exec(f"DELETE FROM yoshop_order_refund WHERE order_refund_id={refund_id}")
|
|
|
+ for order_id in created_recharge_orders:
|
|
|
+ db_exec(f"DELETE FROM yoshop_recharge_order_plan WHERE order_id={order_id}")
|
|
|
+ db_exec(f"DELETE FROM yoshop_recharge_order WHERE order_id={order_id}")
|
|
|
+ db_exec("DELETE FROM yoshop_payment WHERE store_id=10001 AND method IN ('wechat','balance') "
|
|
|
+ "AND client='MP-WEIXIN'")
|
|
|
+ db_exec(f"DELETE FROM yoshop_user_coupon WHERE user_id={USER_ID} AND coupon_id={COUPON_ID}")
|
|
|
+ db_exec(f"UPDATE yoshop_coupon SET receive_num={coupon_receive_backup} WHERE coupon_id={COUPON_ID}")
|
|
|
+ if temp_user_id:
|
|
|
+ db_exec(f"DELETE FROM yoshop_user WHERE user_id={temp_user_id}")
|
|
|
+ if user_backup:
|
|
|
+ db_exec("UPDATE yoshop_user SET mobile='{}', nick_name='{}', avatar_id={} WHERE user_id={}".format(
|
|
|
+ user_backup["mobile"], user_backup["nick_name"], user_backup["avatar_id"], USER_ID))
|
|
|
+ for file_id in created_files:
|
|
|
+ db_exec(f"DELETE FROM yoshop_upload_file WHERE file_id={file_id}")
|
|
|
+ print(" CLEAN 测试数据已清理")
|
|
|
+ except Exception as e: # noqa: BLE001
|
|
|
+ print(" CLEAN 失败:", e)
|
|
|
+
|
|
|
+
|
|
|
+def main():
|
|
|
+ global temp_user_id, coupon_receive_backup
|
|
|
+ print("== 客户端 api 第 3 批(用户中心 / 优惠券 / 明细 / 售后 / 充值 / 上传) ==")
|
|
|
+ redis_set("yoshop:api:token:" + TOKEN, USER_ID)
|
|
|
+ row = db(f"SELECT mobile, nick_name, avatar_id FROM yoshop_user WHERE user_id={USER_ID}")[0]
|
|
|
+ user_backup.update(mobile=row[0], nick_name=row[1], avatar_id=row[2])
|
|
|
+ coupon_receive_backup = db(f"SELECT receive_num FROM yoshop_coupon WHERE coupon_id={COUPON_ID}")[0][0]
|
|
|
+
|
|
|
+ # ---------------------------------------------------------------- 未登录
|
|
|
+ print("-- 未登录校验")
|
|
|
+ for path, label in [("/api/user/info", "user/info"), ("/api/user/assets", "user/assets"),
|
|
|
+ ("/api/myCoupon/list", "myCoupon/list"), ("/api/refund/list", "refund/list")]:
|
|
|
+ r = get(path, token=None)
|
|
|
+ check(f"未登录 {label} 返回 401", r.get("status") == 401, r.get("message"))
|
|
|
+
|
|
|
+ # ---------------------------------------------------------------- 用户信息
|
|
|
+ print("-- 用户中心")
|
|
|
+ r = get("/api/user/info")
|
|
|
+ info = field(r.get("data"), "userInfo") or {}
|
|
|
+ check("user/info status=200", r.get("status") == 200, r.get("message"))
|
|
|
+ check("user/info 手机号打码", info.get("mobile") == "185****1340", info.get("mobile"))
|
|
|
+ check("user/info 含 avatarUrl / grade / balance 键",
|
|
|
+ all(k in info for k in ("avatarUrl", "grade", "balance", "points")), list(info.keys()))
|
|
|
+ check("user/info 隐藏 isDelete/storeId/createTime",
|
|
|
+ not any(k in info for k in ("isDelete", "storeId", "createTime", "updateTime", "password")),
|
|
|
+ [k for k in ("isDelete", "storeId", "createTime") if k in info])
|
|
|
+ check("user/info 无 snake_case", not snake_keys(info), snake_keys(info)[:3])
|
|
|
+
|
|
|
+ r = get("/api/user/assets")
|
|
|
+ assets = field(r.get("data"), "assets") or {}
|
|
|
+ check("user/assets 三个字段齐全", set(assets.keys()) == {"balance", "points", "coupon"}, assets.keys())
|
|
|
+ check("user/assets balance 两位小数字符串",
|
|
|
+ isinstance(assets.get("balance"), str) and re.fullmatch(r"-?\d+\.\d{2}", assets.get("balance", "")),
|
|
|
+ assets.get("balance"))
|
|
|
+ check("user/assets 无 snake_case", not snake_keys(assets), snake_keys(assets)[:3])
|
|
|
+
|
|
|
+ # ---------------------------------------------------------------- 明细
|
|
|
+ print("-- 余额 / 积分明细")
|
|
|
+ r = get("/api/balance.log/list")
|
|
|
+ logs = field(r.get("data"), "data") or []
|
|
|
+ check("balance.log/list status=200", r.get("status") == 200, r.get("message"))
|
|
|
+ check("balance.log/list 有数据且按时间倒序",
|
|
|
+ len(logs) > 0 and (len(logs) < 2 or logs[0]["createTime"] >= logs[1]["createTime"]), len(logs))
|
|
|
+ check("balance.log/list 隐藏 storeId 与 user",
|
|
|
+ all("storeId" not in x and "user" not in x for x in logs[:3]), logs[:1])
|
|
|
+ check("balance.log/list perPage=15", field(r.get("data"), "perPage") == 15, field(r.get("data"), "perPage"))
|
|
|
+
|
|
|
+ r = get("/api/points.log/list")
|
|
|
+ plogs = field(r.get("data"), "data") or []
|
|
|
+ check("points.log/list status=200", r.get("status") == 200, r.get("message"))
|
|
|
+ check("points.log/list 字段为 logId/value/describe",
|
|
|
+ all({"logId", "value", "describe"} <= set(x.keys()) for x in plogs[:3]), plogs[:1])
|
|
|
+ check("points.log/list 无 snake_case", not snake_keys(plogs), snake_keys(plogs)[:3])
|
|
|
+
|
|
|
+ # ---------------------------------------------------------------- 修改资料
|
|
|
+ print("-- 修改资料")
|
|
|
+ r = post("/api/user/personal", {"form": {"nickName": "接口自测昵称"}})
|
|
|
+ check("user/personal 修改成功", r.get("status") == 200 and r.get("message") == "恭喜您,信息修改成功", r)
|
|
|
+ nick = db(f"SELECT nick_name, avatar_id FROM yoshop_user WHERE user_id={USER_ID}")[0]
|
|
|
+ check("user/personal 昵称落库", nick[0] == "接口自测昵称", nick)
|
|
|
+ check("user/personal 头像沿用原值", nick[1] == user_backup["avatar_id"], nick[1])
|
|
|
+
|
|
|
+ # ---------------------------------------------------------------- 绑定手机号
|
|
|
+ print("-- 绑定手机号")
|
|
|
+ new_mobile = "19900002222"
|
|
|
+ r = post("/api/user/bindMobile", {"form": {"mobile": new_mobile, "smsCode": "123456"}})
|
|
|
+ check("bindMobile 无验证码时报错", r.get("status") == 500 and "短信验证码不存在" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ redis_set("yoshop:api:sms:" + new_mobile, json.dumps({"code": "654321", "times": 5}))
|
|
|
+ r = post("/api/user/bindMobile", {"form": {"mobile": new_mobile, "smsCode": "000000"}})
|
|
|
+ check("bindMobile 验证码错误时报错", r.get("status") == 500 and "验证码不正确" in (r.get("message") or ""), r)
|
|
|
+ left = json.loads(db_redis("yoshop:api:sms:" + new_mobile) or "null")
|
|
|
+ check("bindMobile 错误一次后剩余次数减一", left and left.get("times") == 4, left)
|
|
|
+
|
|
|
+ r = post("/api/user/bindMobile", {"form": {"mobile": new_mobile, "smsCode": "654321"}})
|
|
|
+ check("bindMobile 绑定成功", r.get("status") == 200 and r.get("message") == "恭喜您,手机号绑定成功", r)
|
|
|
+ check("bindMobile 手机号落库", db(f"SELECT mobile FROM yoshop_user WHERE user_id={USER_ID}")[0][0] == new_mobile)
|
|
|
+ check("bindMobile 验证码用后即焚", db_redis("yoshop:api:sms:" + new_mobile) is None)
|
|
|
+
|
|
|
+ # 手机号已被占用
|
|
|
+ temp_user_id = db_exec(
|
|
|
+ "INSERT INTO yoshop_user (mobile, nick_name, avatar_id, gender, country, province, city, address_id, "
|
|
|
+ "balance, points, pay_money, expend_money, grade_id, platform, last_login_time, is_delete, store_id, "
|
|
|
+ "create_time, update_time) VALUES ('19900003333', '临时用户', 0, 0, '', '', '', 0, 0.00, 0, 0.00, "
|
|
|
+ "0.00, 0, 'H5', 0, 0, 10001, UNIX_TIMESTAMP(), UNIX_TIMESTAMP())")
|
|
|
+ redis_set("yoshop:api:sms:19900003333", json.dumps({"code": "111111", "times": 5}))
|
|
|
+ r = post("/api/user/bindMobile", {"form": {"mobile": "19900003333", "smsCode": "111111"}})
|
|
|
+ check("bindMobile 手机号已占用时报错",
|
|
|
+ r.get("status") == 500 and "已绑定其他账户" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ # ---------------------------------------------------------------- 我的优惠券
|
|
|
+ print("-- 我的优惠券")
|
|
|
+ r = post("/api/myCoupon/receive", {"couponId": 999999})
|
|
|
+ check("myCoupon/receive 券不存在时报错",
|
|
|
+ r.get("status") == 500 and "优惠券不存在" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ r = post("/api/myCoupon/receive", {"couponId": COUPON_ID})
|
|
|
+ check("myCoupon/receive 领取成功", r.get("status") == 200 and r.get("message") == "领取成功", r)
|
|
|
+ recv = db(f"SELECT user_coupon_id, name, is_use, is_expire, start_time, end_time "
|
|
|
+ f"FROM yoshop_user_coupon WHERE user_id={USER_ID} AND coupon_id={COUPON_ID}")
|
|
|
+ check("领取记录落库且未使用未过期", len(recv) == 1 and recv[0][2] == 0 and recv[0][3] == 0, recv)
|
|
|
+ check("领取后有效期为 7 天", len(recv) == 1 and recv[0][5] - recv[0][4] == 7 * 86400, recv)
|
|
|
+ check("券已领取数量 +1",
|
|
|
+ db(f"SELECT receive_num FROM yoshop_coupon WHERE coupon_id={COUPON_ID}")[0][0]
|
|
|
+ == coupon_receive_backup + 1)
|
|
|
+
|
|
|
+ r = post("/api/myCoupon/receive", {"couponId": COUPON_ID})
|
|
|
+ check("myCoupon/receive 重复领取被拒",
|
|
|
+ r.get("status") == 500 and "已领取该优惠券" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ r = get("/api/myCoupon/list")
|
|
|
+ data = field(r.get("data"), "data") or []
|
|
|
+ check("myCoupon/list 含刚领取的券", any(x.get("couponId") == COUPON_ID for x in data), len(data))
|
|
|
+ check("myCoupon/list 隐藏 storeId/createTime/updateTime",
|
|
|
+ all(not {"storeId", "createTime", "updateTime"} & set(x.keys()) for x in data), data[:1])
|
|
|
+ check("myCoupon/list state 为正常", data and field(data[0].get("state"), "text") == "正常", data[:1])
|
|
|
+ check("myCoupon/list discount 为 0-10 小数", all(isinstance(x.get("discount"), (int, float)) for x in data),
|
|
|
+ [x.get("discount") for x in data[:2]])
|
|
|
+
|
|
|
+ r = get("/api/myCoupon/list", {"dataType": "isUsable"})
|
|
|
+ usable = field(r.get("data"), "data") or []
|
|
|
+ check("myCoupon/list isUsable 含该券", any(x.get("couponId") == COUPON_ID for x in usable), len(usable))
|
|
|
+
|
|
|
+ r = get("/api/myCoupon/list", {"dataType": "isUsable", "amount": 50})
|
|
|
+ filtered = field(r.get("data"), "data") or []
|
|
|
+ check("myCoupon/list amount=50 过滤掉满 100 的券",
|
|
|
+ not any(x.get("couponId") == COUPON_ID for x in filtered), len(filtered))
|
|
|
+
|
|
|
+ r = get("/api/user/assets")
|
|
|
+ check("user/assets 可用券数量 +1", field(field(r.get("data"), "assets"), "coupon") == 1,
|
|
|
+ field(field(r.get("data"), "assets"), "coupon"))
|
|
|
+
|
|
|
+ # ---------------------------------------------------------------- 售后
|
|
|
+ print("-- 售后服务")
|
|
|
+ order_id, order_goods_id = make_order()
|
|
|
+ r = get("/api/refund/goods", {"orderGoodsId": order_goods_id})
|
|
|
+ goods = field(r.get("data"), "goods") or {}
|
|
|
+ check("refund/goods status=200", r.get("status") == 200, r.get("message"))
|
|
|
+ check("refund/goods 含商品图与金额", "goodsImage" in goods and goods.get("totalPayPrice") == "80.00",
|
|
|
+ {k: goods.get(k) for k in ("goodsImage", "totalPayPrice")})
|
|
|
+ check("refund/goods 无 snake_case", not snake_keys(goods), snake_keys(goods)[:3])
|
|
|
+
|
|
|
+ r = get("/api/refund/goods", {"orderGoodsId": 99999999})
|
|
|
+ check("refund/goods 不存在时报错", r.get("status") == 500 and "未找到订单商品" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ r = post("/api/refund/apply", {"orderGoodsId": order_goods_id, "form": {"content": "不想要了"}})
|
|
|
+ check("refund/apply 缺少类型时报错", r.get("status") == 500 and "请选择售后类型" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ r = post("/api/refund/apply", {"orderGoodsId": order_goods_id,
|
|
|
+ "form": {"type": 10, "content": "接口自测:商品有瑕疵"}})
|
|
|
+ check("refund/apply 提交成功", r.get("status") == 200 and r.get("message") == "提交成功", r)
|
|
|
+ refund_rows = db(f"SELECT order_refund_id, type, apply_desc, audit_status, status, is_user_send "
|
|
|
+ f"FROM yoshop_order_refund WHERE order_goods_id={order_goods_id}")
|
|
|
+ check("售后单落库为待审核", len(refund_rows) == 1 and refund_rows[0][3] == 0 and refund_rows[0][4] == 0,
|
|
|
+ refund_rows)
|
|
|
+ refund_id = refund_rows[0][0]
|
|
|
+ created_refunds.append(refund_id)
|
|
|
+
|
|
|
+ r = get("/api/refund/goods", {"orderGoodsId": order_goods_id})
|
|
|
+ check("已申请售后不可重复申请", r.get("status") == 500 and "已申请售后" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ r = get("/api/refund/detail", {"orderRefundId": refund_id})
|
|
|
+ detail = field(r.get("data"), "detail") or {}
|
|
|
+ check("refund/detail stateText=等待审核中", detail.get("stateText") == "等待审核中", detail.get("stateText"))
|
|
|
+ check("refund/detail 含 orderGoods 且图片为空数组",
|
|
|
+ isinstance(detail.get("orderGoods"), dict) and detail.get("images") == [], detail.get("images"))
|
|
|
+ check("refund/detail 隐藏 storeId/updateTime",
|
|
|
+ not {"storeId", "updateTime"} & set(detail.keys()), [k for k in ("storeId", "updateTime") if k in detail])
|
|
|
+ check("refund/detail 无 snake_case", not snake_keys(detail), snake_keys(detail)[:3])
|
|
|
+
|
|
|
+ r = get("/api/refund/list", {"state": 0})
|
|
|
+ refund_list = field(r.get("data"), "data") or []
|
|
|
+ check("refund/list 含该售后单", any(x.get("orderRefundId") == refund_id for x in refund_list), len(refund_list))
|
|
|
+ check("refund/list 每项均带 stateText", all(x.get("stateText") for x in refund_list), refund_list[:1])
|
|
|
+ check("refund/list 隐藏 storeId/updateTime/images",
|
|
|
+ all(not {"storeId", "updateTime", "images"} & set(x.keys()) for x in refund_list), refund_list[:1])
|
|
|
+
|
|
|
+ r = get("/api/refund/list", {"state": 20})
|
|
|
+ check("refund/list state=20 不含进行中的单",
|
|
|
+ not any(x.get("orderRefundId") == refund_id for x in (field(r.get("data"), "data") or [])), r.get("data"))
|
|
|
+
|
|
|
+ # 未审核不允许发货
|
|
|
+ r = post("/api/refund/delivery", {"orderRefundId": refund_id,
|
|
|
+ "form": {"expressId": EXPRESS_ID, "expressNo": "SF0001"}})
|
|
|
+ check("refund/delivery 未审核时报错", r.get("status") == 500 and "不合法" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ db_exec(f"UPDATE yoshop_order_refund SET audit_status=10 WHERE order_refund_id={refund_id}")
|
|
|
+ r = post("/api/refund/delivery", {"orderRefundId": refund_id, "form": {"expressNo": "SF0001"}})
|
|
|
+ check("refund/delivery 未选物流公司时报错",
|
|
|
+ r.get("status") == 500 and "请选择物流公司" in (r.get("message") or ""), r)
|
|
|
+ r = post("/api/refund/delivery", {"orderRefundId": refund_id, "form": {"expressId": EXPRESS_ID}})
|
|
|
+ check("refund/delivery 未填单号时报错",
|
|
|
+ r.get("status") == 500 and "请填写物流单号" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ r = post("/api/refund/delivery", {"orderRefundId": refund_id,
|
|
|
+ "form": {"expressId": EXPRESS_ID, "expressNo": "SF0001"}})
|
|
|
+ check("refund/delivery 发货成功", r.get("status") == 200 and r.get("message") == "操作成功", r)
|
|
|
+ sent = db(f"SELECT is_user_send, express_id, express_no, send_time FROM yoshop_order_refund "
|
|
|
+ f"WHERE order_refund_id={refund_id}")[0]
|
|
|
+ check("售后单发货信息落库",
|
|
|
+ sent[0] == 1 and str(sent[1]) == str(EXPRESS_ID) and sent[2] == "SF0001" and sent[3] > 0, sent)
|
|
|
+
|
|
|
+ r = get("/api/refund/detail", {"orderRefundId": refund_id})
|
|
|
+ detail = field(r.get("data"), "detail") or {}
|
|
|
+ check("refund/detail 发货后 stateText 变化",
|
|
|
+ detail.get("stateText") == "已发货,待平台确认", detail.get("stateText"))
|
|
|
+ check("refund/detail 带物流公司名称",
|
|
|
+ field(detail.get("express"), "expressName") == "中通快递", detail.get("express"))
|
|
|
+
|
|
|
+ r = get("/api/refund/detail", {"orderRefundId": 99999999})
|
|
|
+ check("refund/detail 不存在时报错", r.get("status") == 500 and "未找到该售后单" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ # ---------------------------------------------------------------- 充值
|
|
|
+ print("-- 余额充值")
|
|
|
+ r = get("/api/recharge/center", {"client": "MP-WEIXIN"})
|
|
|
+ check("recharge/center 无支付方式时报错",
|
|
|
+ r.get("status") == 500 and "没有可用的支付方式" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ make_payment("MP-WEIXIN", "wechat", 1)
|
|
|
+ make_payment("MP-WEIXIN", "balance", 0)
|
|
|
+ r = get("/api/recharge/center", {"client": "MP-WEIXIN"})
|
|
|
+ center = r.get("data") or {}
|
|
|
+ setting = field(center, "setting") or {}
|
|
|
+ check("recharge/center status=200", r.get("status") == 200, r.get("message"))
|
|
|
+ check("recharge/center 设置默认值(isCustom=1 / lowestMoney=10 / isMatchPlan=1)",
|
|
|
+ setting.get("isCustom") == 1 and float(setting.get("lowestMoney")) == 10.0
|
|
|
+ and setting.get("isMatchPlan") == 1, setting)
|
|
|
+ check("recharge/center personal 为当前用户",
|
|
|
+ field(field(center, "personal"), "userId") == USER_ID, field(center, "personal"))
|
|
|
+ check("recharge/center planList 含套餐", any(p.get("planId") == PLAN_ID for p in
|
|
|
+ (field(center, "planList") or [])), field(center, "planList"))
|
|
|
+ check("recharge/center paymentMethods 排除余额支付",
|
|
|
+ [m.get("method") for m in (field(center, "paymentMethods") or [])] == ["wechat"],
|
|
|
+ field(center, "paymentMethods"))
|
|
|
+ check("recharge/center 无 snake_case", not snake_keys(center), snake_keys(center)[:3])
|
|
|
+
|
|
|
+ r = post("/api/recharge/submit", {"form": {"method": "balance", "client": "MP-WEIXIN"}})
|
|
|
+ check("recharge/submit 缺套餐与金额时报错",
|
|
|
+ r.get("status") == 500 and "请选择充值套餐或输入充值金额" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ r = post("/api/recharge/submit", {"form": {"method": "balance", "client": "MP-WEIXIN",
|
|
|
+ "customMoney": "5"}})
|
|
|
+ check("recharge/submit 低于最低金额时报错",
|
|
|
+ r.get("status") == 500 and "最低充值金额不能低于10元" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ r = post("/api/recharge/submit", {"form": {"method": "balance", "client": "MP-WEIXIN",
|
|
|
+ "customMoney": "100"}})
|
|
|
+ check("recharge/submit 自定义金额下单成功",
|
|
|
+ r.get("status") == 200 and r.get("message") == "下单成功" and field(r.get("data"), "payment") == [], r)
|
|
|
+ row = db(f"SELECT order_id, order_no, recharge_type, plan_id, pay_price, gift_money, actual_money, "
|
|
|
+ f"pay_status, platform FROM yoshop_recharge_order WHERE user_id={USER_ID} ORDER BY order_id DESC "
|
|
|
+ "LIMIT 1")[0]
|
|
|
+ created_recharge_orders.append(row[0])
|
|
|
+ check("充值订单号以 RC 开头且 18 位以上", str(row[1]).startswith("RC") and len(str(row[1])) >= 18, row[1])
|
|
|
+ check("自定义充值匹配套餐(赠 10)",
|
|
|
+ row[2] == 10 and row[3] == PLAN_ID and float(row[5]) == 10.0 and float(row[6]) == 110.0, row)
|
|
|
+ check("充值订单平台取请求头", row[8] == "MP-WEIXIN", row[8])
|
|
|
+ snapshot = db(f"SELECT plan_id, plan_name, money, gift_money FROM yoshop_recharge_order_plan "
|
|
|
+ f"WHERE order_id={row[0]}")
|
|
|
+ check("套餐快照落库", len(snapshot) == 1 and snapshot[0][0] == PLAN_ID, snapshot)
|
|
|
+
|
|
|
+ r = post("/api/recharge/submit", {"form": {"method": "balance", "client": "MP-WEIXIN",
|
|
|
+ "planId": PLAN_ID}})
|
|
|
+ check("recharge/submit 指定套餐下单成功", r.get("status") == 200, r)
|
|
|
+ row2 = db(f"SELECT order_id, recharge_type, plan_id, pay_price, gift_money, actual_money "
|
|
|
+ f"FROM yoshop_recharge_order WHERE user_id={USER_ID} ORDER BY order_id DESC LIMIT 1")[0]
|
|
|
+ created_recharge_orders.append(row2[0])
|
|
|
+ check("套餐充值金额与赠送正确",
|
|
|
+ row2[1] == 20 and float(row2[3]) == 100.0 and float(row2[4]) == 10.0 and float(row2[5]) == 110.0, row2)
|
|
|
+
|
|
|
+ r = post("/api/recharge/submit", {"form": {"method": "wechat", "client": "MP-WEIXIN",
|
|
|
+ "planId": PLAN_ID}})
|
|
|
+ check("recharge/submit 微信支付未接入时明确报错",
|
|
|
+ r.get("status") == 500 and "第三方支付" in (r.get("message") or ""), r)
|
|
|
+
|
|
|
+ r = get("/api/recharge/tradeQuery", {"outTradeNo": "RC123456", "method": "wechat",
|
|
|
+ "client": "MP-WEIXIN"})
|
|
|
+ check("recharge/tradeQuery 未支付返回 isPay=false",
|
|
|
+ r.get("status") == 200 and field(r.get("data"), "isPay") is False, r)
|
|
|
+ check("recharge/tradeQuery 提示未支付",
|
|
|
+ "订单未支付" in (r.get("message") or ""), r.get("message"))
|
|
|
+
|
|
|
+ db_exec(f"UPDATE yoshop_recharge_order SET pay_status=20, pay_time=UNIX_TIMESTAMP(), pay_method='balance' "
|
|
|
+ f"WHERE order_id={row[0]}")
|
|
|
+ r = get("/api/recharge.order/list")
|
|
|
+ orders = field(r.get("data"), "data") or []
|
|
|
+ check("recharge.order/list 只列已支付订单",
|
|
|
+ [o.get("orderId") for o in orders] == [row[0]], [o.get("orderId") for o in orders])
|
|
|
+ check("recharge.order/list 隐藏 tradeId/createTime/storeId",
|
|
|
+ all(not {"tradeId", "createTime", "storeId"} & set(o.keys()) for o in orders), orders[:1])
|
|
|
+ check("recharge.order/list 金额为两位小数字符串",
|
|
|
+ orders and orders[0].get("actualMoney") == "110.00", orders[:1])
|
|
|
+ check("recharge.order/list 无 snake_case", not snake_keys(orders), snake_keys(orders)[:3])
|
|
|
+
|
|
|
+ # ---------------------------------------------------------------- 上传
|
|
|
+ print("-- 文件上传")
|
|
|
+ r = upload("/api/upload/image", token=None, check_login=1)
|
|
|
+ check("upload/image 需要登录时未登录返回 401", r.get("status") == 401, r)
|
|
|
+
|
|
|
+ r = upload("/api/upload/image", check_login=1)
|
|
|
+ file_info = field(r.get("data"), "fileInfo") or {}
|
|
|
+ check("upload/image 上传成功", r.get("status") == 200 and r.get("message") == "文件上传成功", r)
|
|
|
+ check("upload/image 返回 fileId 与 previewUrl",
|
|
|
+ isinstance(file_info.get("fileId"), int) and str(file_info.get("previewUrl", "")).startswith("http"),
|
|
|
+ file_info)
|
|
|
+ check("upload/image 隐藏 filePath/fileName/storeId",
|
|
|
+ not {"filePath", "fileName", "storeId", "uploaderId"} & set(file_info.keys()), list(file_info.keys()))
|
|
|
+ if file_info.get("fileId"):
|
|
|
+ created_files.append(file_info["fileId"])
|
|
|
+ uploader = db(f"SELECT channel, uploader_id, file_type, is_delete FROM yoshop_upload_file "
|
|
|
+ f"WHERE file_id={file_info['fileId']}")[0]
|
|
|
+ check("上传记录 channel=20 且记录上传者", uploader[0] == 20 and uploader[1] == USER_ID, uploader)
|
|
|
+
|
|
|
+ r = upload("/api/upload/image", token=None, check_login=0)
|
|
|
+ anon_info = field(r.get("data"), "fileInfo") or {}
|
|
|
+ check("upload/image checkLogin=0 允许匿名上传", r.get("status") == 200, r)
|
|
|
+ if anon_info.get("fileId"):
|
|
|
+ created_files.append(anon_info["fileId"])
|
|
|
+ check("匿名上传 uploader_id=0",
|
|
|
+ db(f"SELECT uploader_id FROM yoshop_upload_file WHERE file_id={anon_info['fileId']}")[0][0] == 0)
|
|
|
+
|
|
|
+ # 用上传的文件改头像
|
|
|
+ if file_info.get("fileId"):
|
|
|
+ r = post("/api/user/personal", {"form": {"avatarId": file_info["fileId"]}})
|
|
|
+ check("user/personal 设置头像成功", r.get("status") == 200, r)
|
|
|
+ r = get("/api/user/info")
|
|
|
+ avatar_url = field(field(r.get("data"), "userInfo"), "avatarUrl")
|
|
|
+ check("user/info 头像地址随之上线", isinstance(avatar_url, str) and avatar_url.startswith("http"),
|
|
|
+ avatar_url)
|
|
|
+ check("user/info avatar 对象非空", field(field(r.get("data"), "userInfo"), "avatar") is not None)
|
|
|
+
|
|
|
+ print(f"\n结果:PASS {passed} / FAIL {failed}")
|
|
|
+ return 1 if failed else 0
|
|
|
+
|
|
|
+
|
|
|
+def db_redis(key):
|
|
|
+ sock = socket.create_connection(("192.168.1.177", 6379), timeout=5)
|
|
|
+ try:
|
|
|
+ cmd(sock, "AUTH", REDIS_PWD)
|
|
|
+ result = cmd(sock, "GET", key)
|
|
|
+ finally:
|
|
|
+ sock.close()
|
|
|
+ return result if isinstance(result, str) else None
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == "__main__":
|
|
|
+ try:
|
|
|
+ code = main()
|
|
|
+ finally:
|
|
|
+ cleanup()
|
|
|
+ sys.exit(code)
|