Files
baiyue-firstleg/scripts/client-import-template-inspect.py
T

197 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Read-only TEST template audit. Credentials and workbook bytes stay in memory."""
import hashlib
import io
import json
import re
import sys
import urllib.error
import urllib.parse
import urllib.request
from datetime import datetime, timezone
from pathlib import Path
from openpyxl import load_workbook
def require(value, message):
if not value:
raise AssertionError(message)
def labels(values, labeler):
result = []
for item in values:
base = labeler(item)
label, n = base, 2
while label in result:
label = f"{base}(同名选项 {n})"
n += 1
result.append(label)
return result
def label(name, code):
return code if not name or name.lower() == code.lower() else f"{name}|{code}"
def compare_options(actual, expected):
return {"expectedCount": len(expected), "actualCount": len(actual),
"missingCount": len(set(expected) - set(actual)),
"extraCount": len(set(actual) - set(expected)),
"duplicateCount": len(actual) - len(set(actual)),
"passed": sorted(actual) == sorted(expected),
"actualSha256": hashlib.sha256(json.dumps(sorted(actual), ensure_ascii=False).encode()).hexdigest()}
def inspect_template(content, expected):
book = load_workbook(io.BytesIO(content), data_only=False)
try:
require(set(book.sheetnames) == {"订单导入", "选项数据", "填写说明"}, "Unexpected workbook sheets")
require(book["选项数据"].sheet_state == "hidden", "Options sheet must be hidden")
sheet = book["订单导入"]
headers = {str(cell.value).removesuffix(" *"): cell.column for cell in sheet[2] if cell.value}
checks = {}
for title, values in expected.items():
require(title in headers, "Missing expected header")
col = headers[title]
validations = [v for v in sheet.data_validations.dataValidation
if any(r.min_col <= col <= r.max_col and r.min_row <= 3 <= r.max_row for r in v.sqref.ranges)]
if not values:
require(not validations, "Empty customer directory unexpectedly has choices")
checks[title] = compare_options([], [])
continue
require(len(validations) == 1, "Missing or overlapping dropdown validation")
validation = validations[0]
require(validation.type == "list" and validation.showErrorMessage, "Dropdown must reject invalid choices")
require(any(r.min_row == 3 and r.max_row == 52 and r.min_col == col and r.max_col == col
for r in validation.sqref.ranges), "Dropdown must cover all 50 input rows")
name = book.defined_names.get(validation.formula1)
require(name is not None, "Dropdown name does not resolve")
destinations = list(name.destinations)
require(len(destinations) == 1 and destinations[0][0] == "选项数据", "Invalid dropdown destination")
actual = [cell.value for row in book[destinations[0][0]][destinations[0][1]] for cell in row]
require(all(isinstance(v, str) and v for v in actual), "Dropdown contains blank or invalid entries")
checks[title] = compare_options(actual, values)
return {"checks": checks, "passed": all(v["passed"] for v in checks.values()),
"workbookSha256": hashlib.sha256(content).hexdigest(), "headerCount": len(headers),
"dropdowns": len(sheet.data_validations.dataValidation)}
finally:
book.close()
def main(config):
require(config.get("environment") == "TEST" and re.fullmatch(r"[A-Za-z0-9_-]{1,60}", config.get("runId", "")), "Explicit TEST run required")
directory = Path(__file__).resolve().parent.parent / "reports" / ("client-template-" + config["runId"])
directory.mkdir() # Never overwrite an earlier run.
report_path = directory / "report.json"
report = {"startedAt": datetime.now(timezone.utc).isoformat(), "readOnlyBusiness": True,
"success": False, "sessionClosed": False, "requests": []}
token, refresh = "", ""
def save():
report_path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
def request(method, path, body=None, binary=False):
require(method == "GET" or method == "POST" and path in ["/auth/login", "/auth/logout"], "Business writes forbidden")
parsed = urllib.parse.urlsplit(path)
query = urllib.parse.parse_qs(parsed.query)
record = {"method": method, "path": parsed.path, "outcome": "PENDING",
"page": query.get("page"), "size": query.get("size")}
report["requests"].append(record)
save()
headers = {"Accept": "application/json"}
if token:
headers["Authorization"] = "Bearer " + token
if body is not None:
headers["Content-Type"] = "application/json"
req = urllib.request.Request(config["base"] + path, method=method, headers=headers,
data=json.dumps(body).encode() if body is not None else None)
try:
with urllib.request.urlopen(req, timeout=30) as response:
data = response.read()
record.update(outcome="RECEIVED", http=response.status,
requestId=response.headers.get("X-Request-ID"), traceId=response.headers.get("X-Trace-ID"))
if binary:
require(data[:2] == b"PK", "Template response is not XLSX")
record["sha256"] = hashlib.sha256(data).hexdigest()
save()
return data
payload = json.loads(data)
record.update(code=payload.get("code"), requestId=payload.get("requestId") or record["requestId"],
traceId=payload.get("traceId") or record["traceId"])
save()
require(str(payload.get("code")) == "200", "API business response rejected")
return payload["data"]
except urllib.error.HTTPError as error:
record.update(outcome="RECEIVED", http=error.code)
save()
raise AssertionError("HTTP response rejected") from None
except Exception:
if record["outcome"] == "PENDING":
record["outcome"] = "UNKNOWN"
save()
raise
try:
auth = request("POST", "/auth/login", {"loginId": config["username"], "password": config["password"]})
token, refresh = auth.get("accessToken", ""), auth.get("refreshToken", "")
require(token and refresh, "Incomplete login")
profile = request("GET", "/merchant/v1/iam/tenant/profile")
require(profile.get("id") is not None, "Missing authenticated customer identity")
report["customerIdentitySha256"] = hashlib.sha256(str(profile["id"]).encode()).hexdigest()
warehouses, total, page_no = [], None, 1
while True:
page = request("GET", f"/merchant/v1/warehouses?status=ACTIVE&page={page_no}&size=200")
if total is None:
total = int(page["total"])
require(int(page["page"]) == page_no and int(page["size"]) == 200 and int(page["total"]) == total
and int(page["pages"]) == (total + 199) // 200, "Warehouse pagination changed")
require(len(page["records"]) == min(200, max(0, total - (page_no - 1) * 200)), "Warehouse page incomplete")
warehouses.extend(page["records"])
if page_no >= int(page["pages"]):
break
require(page_no < 1000, "Warehouse page limit exceeded")
page_no += 1
require(len({str(w["id"]) for w in warehouses}) == total and all(w["status"] == "ACTIVE" for w in warehouses), "Invalid warehouse set")
expected = {}
for kind, title in [("DOMESTIC", "国内收货仓"), ("FBA", "FBA目的仓"), ("OVERSEAS", "海外仓目的仓")]:
expected[title] = labels([w for w in warehouses if w["warehouseType"] == kind], lambda w: label(w["warehouseName"], w["warehouseCode"]))
channels = []
for warehouse in warehouses:
if warehouse["warehouseType"] != "DOMESTIC":
continue
values = request("GET", "/merchant/v1/first-leg/pricing/channels?originWarehouseId=" + str(warehouse["id"]))
channels.extend(label(c["channelName"], c["channelCode"]) for c in values)
expected["运输渠道"] = list(dict.fromkeys(channels))
for kind, title in [("PICKUP", "取货地址"), ("PRIVATE_DESTINATION", "私人目的仓地址")]:
addresses = request("GET", "/merchant/v1/orders/first-leg/addresses?addressType=" + kind)
expected[title] = labels(addresses, lambda a: "|".join(a[key] for key in ["addressName", "contactName", "city", "addressLine"]))
content = request("GET", "/merchant/v1/orders/first-leg/batch-imports/template", binary=True)
report["template"] = inspect_template(content, expected)
report["warehousePages"] = page_no
report["warehouseTotal"] = total
report["success"] = report["template"]["passed"]
report["limitations"] = ["No import submission or Excel desktop interaction performed",
"Second-customer address rejection requires separate evidence"]
except Exception as error:
report["failure"] = str(error) if isinstance(error, AssertionError) else type(error).__name__
finally:
if token:
try:
request("POST", "/auth/logout", {"refreshToken": refresh})
report["sessionClosed"] = True
except Exception:
report["logoutFailure"] = True
report["success"] = report["success"] and report["sessionClosed"]
report["completedAt"] = datetime.now(timezone.utc).isoformat()
save()
print(json.dumps({"success": report["success"], "sessionClosed": report["sessionClosed"], "report": str(report_path)}))
return 0 if report["success"] else 1
if __name__ == "__main__":
try:
sys.exit(main(json.load(sys.stdin)))
except Exception:
print("Template audit did not start; verify the new report directory and runtime.", file=sys.stderr)
sys.exit(1)