| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- # -*- coding: utf-8 -*-
- """客户端 api 第 1 批接口冒烟:只读接口结构 + 关键字段断言。
- 用法:起 Java 18080 后执行
- python tools/test_api_batch1.py
- 环境变量:YOSHOP_API_BASE(默认 http://127.0.0.1:18080)、YOSHOP_TOKEN(已登录用户 token,可选)
- """
- import json
- import os
- import re
- import sys
- import pymysql
- import requests
- from _root import ROOT
- BASE = os.environ.get("YOSHOP_API_BASE", "http://127.0.0.1:18080")
- TOKEN = os.environ.get("YOSHOP_TOKEN", "").strip()
- YML = str(ROOT / "yoshop-java/src/main/resources/application.yml")
- PWD = re.findall(r"password:\s*(.+)", open(YML, encoding="utf-8").read())[0].strip().strip("\"'")
- S = requests.Session()
- S.trust_env = False
- passed = 0
- failed = 0
- 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 get(path, params=None, token=None):
- headers = {"platform": "MP-WEIXIN"}
- if token:
- headers["Access-Token"] = token
- resp = S.get(BASE + path, params=params or {}, headers=headers, timeout=30)
- try:
- return resp.json()
- except Exception:
- return {"status": resp.status_code, "raw": resp.text[:200]}
- def post(path, body=None, token=None):
- headers = {"platform": "MP-WEIXIN"}
- if token:
- headers["Access-Token"] = token
- resp = S.post(BASE + path, json=body or {}, headers=headers, timeout=30)
- try:
- return resp.json()
- except Exception:
- return {"status": resp.status_code, "raw": resp.text[:200]}
- def snake_keys(obj, prefix="$"):
- """收集所有 snake_case 键"""
- 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=PWD,
- database="shop", charset="utf8mb4", autocommit=True)
- try:
- cur = conn.cursor()
- cur.execute(sql)
- return cur.fetchall()
- finally:
- conn.close()
- def main():
- print("== 客户端 api 第 1 批(基础 / 内容 / 商品 / 营销 只读) ==")
- # ---------- 基础信息
- r = get("/api/store/data")
- data = r.get("data") or {}
- check("store/data status=200", r.get("status") == 200, r.get("message"))
- check("store/data 含 storeInfo/client/setting/clientData",
- all(k in data for k in ("storeInfo", "client", "setting", "clientData")), list(data))
- check("store/data client=MP-WEIXIN", data.get("client") == "MP-WEIXIN", data.get("client"))
- store_info = data.get("storeInfo") or {}
- check("storeInfo 隐藏 sort/isRecycle/isDelete", not any(
- k in store_info for k in ("sort", "isRecycle", "isDelete")), list(store_info))
- check("storeInfo 含 imageUrl 键", "imageUrl" in store_info, list(store_info))
- check("clientData.h5.setting 存在", "setting" in ((data.get("clientData") or {}).get("h5") or {}))
- r = get("/api/setting/data")
- setting = r.get("data") or {}
- check("setting/data 六个公共项", set(setting) == {
- "appTheme", "pageCategoryTemplate", "points", "recharge", "register", "customer"}, list(setting))
- check("setting/data register 有 registerMethod",
- "registerMethod" in (setting.get("register") or {}), setting.get("register"))
- # ---------- 地区 / 分类 / 内容
- r = get("/api/region/all")
- region_all = (r.get("data") or {}).get("list") or {}
- check("region/all 非空", len(region_all) > 3000, len(region_all))
- r = get("/api/region/tree")
- region_tree = (r.get("data") or {}).get("list") or {}
- check("region/tree 省级 34 条", len(region_tree) >= 30, len(region_tree))
- r = get("/api/category/list")
- cats = (r.get("data") or {}).get("list") or []
- check("category/list 是数组", isinstance(cats, list), type(cats))
- check("category/list 有子分类或至少 1 条", len(cats) >= 1, len(cats))
- if cats:
- keys = set(cats[0])
- check("分类隐藏 storeId/createTime/updateTime",
- not ({"storeId", "createTime", "updateTime"} & keys), keys)
- # 文章:库里现有文章已被软删除,临时插一条用于验证,跑完删除
- article_id = None
- try:
- db("INSERT INTO yoshop_article (title, show_type, category_id, image_id, content, sort, status, "
- "virtual_views, actual_views, is_delete, store_id, create_time, update_time) "
- "VALUES ('api自测文章', 10, 0, 0, '<p>正文</p>', 100, 1, 0, 0, 0, 10001, "
- "UNIX_TIMESTAMP(), UNIX_TIMESTAMP())")
- article_id = db("SELECT MAX(article_id) FROM yoshop_article")[0][0]
- r = get("/api/article.category/list")
- check("article.category/list status=200", r.get("status") == 200, r.get("message"))
- r = get("/api/article/list")
- page_list = (r.get("data") or {}).get("list") or {}
- check("article/list status=200 且分页结构", r.get("status") == 200
- and all(k in page_list for k in ("total", "perPage", "currentPage", "lastPage")),
- r.get("message"))
- check("article/list 命中临时文章", any(a.get("articleId") == article_id for a in (page_list.get("data") or [])))
- r = get("/api/article/detail", {"articleId": article_id})
- article = (r.get("data") or {}).get("detail") or {}
- check("article/detail 返回文章", article.get("articleId") == article_id,
- article.get("message") or r.get("message"))
- check("文章隐藏 virtualViews/actualViews",
- "actualViews" not in article and "virtualViews" not in article)
- check("文章 content 已反转义", "<" not in str(article.get("content")), article.get("content"))
- views = db(f"SELECT actual_views FROM yoshop_article WHERE article_id={article_id}")[0][0]
- check("文章阅读数 +1", views == 1, views)
- finally:
- if article_id:
- db(f"DELETE FROM yoshop_article WHERE article_id={article_id}")
- r = get("/api/help/list")
- check("help/list status=200", r.get("status") == 200, r.get("message"))
- # ---------- 页面
- r = get("/api/page/detail")
- page_data = (r.get("data") or {}).get("pageData") or {}
- check("page/detail 默认首页有 page/items", "page" in page_data and "items" in page_data, list(page_data))
- items = page_data.get("items") or []
- check("页面组件已去掉 defaultData/demo",
- all("defaultData" not in it and "demo" not in it for it in items))
- # ---------- 商品
- r = get("/api/goods/list")
- page = (r.get("data") or {}).get("list") or {}
- goods_list = page.get("data") or []
- check("goods/list 有数据", len(goods_list) > 0, r.get("message"))
- check("goods/list 分页 15", page.get("perPage") == 15, page.get("perPage"))
- if goods_list:
- g = goods_list[0]
- check("列表隐藏 content/goodsImages/images",
- not ({"content", "goodsImages", "images"} & set(g)), set(g) & {"content", "goodsImages", "images"})
- check("列表含 goodsImage/goodsSales/isUserGrade",
- all(k in g for k in ("goodsImage", "goodsSales", "isUserGrade")), list(g))
- goods_ids = [g["goodsId"] for g in goods_list[:3]]
- r = get("/api/goods/detail", {"goodsId": 10004})
- detail = (r.get("data") or {}).get("detail") or {}
- check("goods/detail 多规格含 skuList/specList",
- bool(detail.get("skuList")) and bool(detail.get("specList")), list(detail))
- check("详情隐藏 images", "images" not in detail, "images" in detail)
- check("详情含 goodsImages", "goodsImages" in detail, list(detail))
- check("详情 content 已反转义(不含 <)", "<" not in str(detail.get("content")))
- r = get("/api/goods/detail", {"goodsId": 999999})
- check("goods/detail 不存在时 status=500", r.get("status") == 500, r.get("status"))
- r = get("/api/goods/basic", {"goodsId": 10003})
- basic = (r.get("data") or {}).get("detail") or {}
- check("goods/basic 不含 skuList/video", "skuList" not in basic and "video" not in basic, list(basic))
- check("goods/basic 含 content", "content" in basic)
- r = get("/api/goods/spec_data", {"goodsId": 10004})
- spec = (r.get("data") or {}).get("specData") or {}
- check("goods/spec_data 含 skuList/specList", bool(spec.get("skuList")) and bool(spec.get("specList")))
- sku0 = (spec.get("skuList") or [{}])[0]
- check("skuList 隐藏 image/storeId/createTime", not ({"image", "storeId", "createTime"} & set(sku0)), set(sku0))
- check("skuList 含 imageUrl", "imageUrl" in sku0, list(sku0))
- sku_id = sku0.get("goodsSkuId")
- if sku_id:
- r = get("/api/goods/sku_info", {"goodsId": 10004, "goodsSkuId": sku_id})
- sku_info = (r.get("data") or {}).get("skuInfo") or {}
- check("goods/sku_info 命中 SKU", sku_info.get("goodsSkuId") == sku_id, sku_info)
- r = get("/api/goods.service/list", {"goodsId": 10004})
- check("goods.service/list status=200", r.get("status") == 200, r.get("message"))
- services = (r.get("data") or {}).get("list") or []
- if services:
- check("服务承诺隐藏 status/sort/isDefault",
- not ({"status", "sort", "isDefault"} & set(services[0])), set(services[0]))
- # ---------- 评价 / 优惠券 / 快递
- for path, key in [("/api/comment/list", "list"), ("/api/comment/list_rows", "list")]:
- r = get(path, {"goodsId": 10003})
- check(f"{path} status=200", r.get("status") == 200, r.get("message"))
- r = get("/api/comment/total", {"goodsId": 10003})
- total = r.get("data") or {}
- check("comment/total 四个统计项", set(total) == {"all", "praise", "review", "negative"}, total)
- r = get("/api/coupon/list")
- coupons = (r.get("data") or {}).get("list") or []
- check("coupon/list 有数据", len(coupons) >= 1, r.get("message"))
- if coupons:
- c = coupons[0]
- check("优惠券隐藏 totalNum/receiveNum/status",
- not ({"totalNum", "receiveNum", "status"} & set(c)), set(c))
- check("未登录不返回 isReceive", "isReceive" not in c, list(c))
- r = get("/api/express/list")
- express = (r.get("data") or {}).get("list") or []
- check("express/list 有数据", len(express) > 0, r.get("message"))
- if express:
- check("快递隐藏 kuaidi100Code/kdniaoCode",
- not ({"kuaidi100Code", "kdniaoCode"} & set(express[0])), set(express[0]))
- # ---------- 登录态相关(带 token 才有 isReceive / 会员价)
- if TOKEN:
- r = get("/api/coupon/list", token=TOKEN)
- coupons = (r.get("data") or {}).get("list") or []
- if coupons:
- check("登录后 coupon/list 返回 isReceive", "isReceive" in coupons[0], list(coupons[0]))
- else:
- print(" SKIP 带 token 的 isReceive 检查(未设置 YOSHOP_TOKEN)")
- # ---------- 驼峰检查
- for path, params in [("/api/goods/list", {}), ("/api/store/data", {}),
- ("/api/goods/detail", {"goodsId": 10004}),
- ("/api/category/list", {})]:
- r = get(path, params)
- bad = snake_keys(r.get("data"))
- check(f"{path} 无 snake_case 键", not bad, bad[:5])
- print(f"\n结果:{passed} passed, {failed} failed")
- return 0 if failed == 0 else 1
- if __name__ == "__main__":
- sys.exit(main())
|