You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
354 lines
19 KiB
354 lines
19 KiB
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""岗位与企业展示字段批次:HTTP 黑盒验收(针对本批隔离 JAR)。
|
|
|
|
仅访问隔离环境(默认 http://127.0.0.1:18080/api/v1),写操作只发生在 b01-app-mysql 隔离库。
|
|
不改认证/安全策略;不打印 Token 原文。
|
|
"""
|
|
import argparse
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
CONTAINER = "b01-app-mysql"
|
|
DB = "mydreamjob_b01_demo"
|
|
DB_USER = "b01demo"
|
|
DB_PASS = "b01-demo-only"
|
|
PHONE_PREFIX = "19902"
|
|
CODE = "246810"
|
|
API = "/api/v1"
|
|
|
|
rows = []
|
|
|
|
|
|
def record(section, case, expected, actual, ok, note=""):
|
|
rows.append({"section": section, "case": case, "expected": expected,
|
|
"actual": str(actual)[:300], "ok": bool(ok), "note": note})
|
|
print("[%s] %-8s %-52s exp=%-26s act=%s %s" % (
|
|
"PASS" if ok else "FAIL", section, case, expected, str(actual)[:80], note))
|
|
return ok
|
|
|
|
|
|
def http(base, method, path, token=None, body=None, raw=None, timeout=30):
|
|
headers = {}
|
|
data = None
|
|
if raw is not None:
|
|
data = raw
|
|
headers["Content-Type"] = "application/json"
|
|
elif body is not None:
|
|
data = json.dumps(body).encode()
|
|
headers["Content-Type"] = "application/json"
|
|
if token:
|
|
headers["Authorization"] = "Bearer " + token
|
|
req = urllib.request.Request(base + path, data=data, headers=headers, method=method)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
return resp.status, resp.read().decode("utf-8", "replace")
|
|
except urllib.error.HTTPError as e:
|
|
return e.code, e.read().decode("utf-8", "replace")
|
|
except Exception as e:
|
|
return -1, "CONNECTION_ERROR: %s" % type(e).__name__
|
|
|
|
|
|
def mysql(sql, db=DB):
|
|
p = subprocess.run(["docker", "exec", "-i", CONTAINER, "mysql",
|
|
"-u" + DB_USER, "-p" + DB_PASS, "--default-character-set=utf8mb4",
|
|
"-N", "-B", db, "-e", sql],
|
|
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
|
if p.returncode != 0:
|
|
raise RuntimeError("mysql failed: %s" % (p.stderr or "").strip()[:300])
|
|
return [ln.split("\t") for ln in p.stdout.strip().splitlines() if ln != ""]
|
|
|
|
|
|
def scalar(sql):
|
|
r = mysql(sql)
|
|
return r[0][0] if r else None
|
|
|
|
|
|
def phone(n):
|
|
return "%s%06d" % (PHONE_PREFIX, n)
|
|
|
|
|
|
def seed_sms(p):
|
|
mysql("INSERT INTO sms_verification_codes (phone, code, purpose, attempts, max_attempts, "
|
|
"expired_at, used, created_at) VALUES ('%s','%s','login',0,5,"
|
|
"DATE_ADD(NOW(), INTERVAL 1 DAY),0,NOW());" % (p, CODE))
|
|
|
|
|
|
def login(base, p):
|
|
seed_sms(p)
|
|
st, body = http(base, "POST", API + "/auth/login", body={"phone": p, "code": CODE, "purpose": "login"})
|
|
if st != 200:
|
|
raise RuntimeError("login failed for %s: %s %s" % (p, st, body[:200]))
|
|
return json.loads(body)["data"]
|
|
|
|
|
|
def as_company(base, p):
|
|
data = login(base, p)
|
|
uid = scalar("SELECT id FROM users WHERE phone='%s'" % p)
|
|
mysql("UPDATE users SET role='COMPANY' WHERE id=%s; INSERT IGNORE INTO user_roles(user_id, role) "
|
|
"VALUES (%s,'COMPANY');" % (uid, uid))
|
|
return data["accessToken"], int(uid)
|
|
|
|
|
|
def jbody(body):
|
|
try:
|
|
return json.loads(body)
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def data_of(body):
|
|
return jbody(body).get("data")
|
|
|
|
|
|
def run(base, api_prefix):
|
|
api = api_prefix
|
|
seq = int(time.time()) % 100000
|
|
|
|
# ── A. 企业资料 + companySize 归一化 ─────────────────────────
|
|
tok_a, uid_a = as_company(base, phone(seq))
|
|
st, body = http(base, "POST", api + "/companies/me", token=tok_a,
|
|
body={"name": "黑盒企业A", "companySize": " 100-499人 ",
|
|
"logoUrl": "https://logo.example/b01.png",
|
|
"description": "黑盒企业简介", "industry": "互联网",
|
|
"website": "https://b01.example"})
|
|
d = data_of(body) or {}
|
|
record("A", "创建 companySize 去空白", "200/100-499人", "%s/%s" % (st, d.get("companySize")),
|
|
st == 200 and d.get("companySize") == "100-499人")
|
|
cid = d.get("id")
|
|
st, body = http(base, "PUT", api + "/companies/me", token=tok_a, body={"name": "黑盒企业A"})
|
|
record("A", "更新缺省保持原值", "100-499人", (data_of(body) or {}).get("companySize"),
|
|
(data_of(body) or {}).get("companySize") == "100-499人")
|
|
st, body = http(base, "GET", api + "/companies/" + str(cid))
|
|
record("A", "公开读取 companySize", "100-499人", (data_of(body) or {}).get("companySize"),
|
|
(data_of(body) or {}).get("companySize") == "100-499人")
|
|
st, _ = http(base, "PUT", api + "/companies/me", token=tok_a,
|
|
body={"name": "黑盒企业A", "companySize": "x" * 41})
|
|
record("A", "companySize 超长 400", 400, st, st == 400)
|
|
st, body = http(base, "PUT", api + "/companies/me", token=tok_a,
|
|
body={"name": "黑盒企业A", "companySize": " "})
|
|
record("A", "空白清空为 null", "None", (data_of(body) or {}).get("companySize"),
|
|
(data_of(body) or {}).get("companySize") in (None, ""))
|
|
mysql("UPDATE companies SET verified=1, company_size='100-499人' WHERE id=%s;" % cid)
|
|
|
|
# ── B. 岗位全字段 + 各接口一致 ───────────────────────────────
|
|
city = "B01CITY" + str(seq)
|
|
full = {"title": "黑盒岗位", "description": "岗位描述", "requirements": "岗位要求",
|
|
"city": city, "employmentType": "FULL_TIME", "salaryMin": 20000, "salaryMax": 40000,
|
|
"experience": "3-5年", "education": "本科", "benefits": ["五险一金", "弹性工作"],
|
|
"salaryMonths": 14, "workLocation": "上海市浦东新区"}
|
|
st, body = http(base, "POST", api + "/jobs", token=tok_a, body=full)
|
|
jid = (data_of(body) or {}).get("id")
|
|
record("B", "全字段创建岗位", "200", st, st == 200)
|
|
st, _ = http(base, "PATCH", api + "/jobs/%s/status" % jid, token=tok_a, body={"status": "PUBLISHED"})
|
|
record("B", "发布岗位", "200", st, st == 200)
|
|
|
|
def check_fields(label, node):
|
|
ok = (node.get("experience") == "3-5年" and node.get("education") == "本科"
|
|
and node.get("salaryMonths") == 14 and node.get("workLocation") == "上海市浦东新区"
|
|
and node.get("benefits") == ["五险一金", "弹性工作"])
|
|
record("B", label, "5 字段一致", "exp=%s ben=%s sm=%s" % (
|
|
node.get("experience"), node.get("benefits"), node.get("salaryMonths")), ok)
|
|
|
|
st, body = http(base, "GET", api + "/jobs?city=" + city)
|
|
check_fields("GET /jobs 字段一致", (data_of(body) or {}).get("items", [{}])[0])
|
|
st, body = http(base, "GET", api + "/jobs/search?city=" + city + "&companyId=" + str(cid))
|
|
check_fields("GET /jobs/search 字段一致", (data_of(body) or {}).get("items", [{}])[0])
|
|
st, body = http(base, "GET", api + "/jobs/public/" + str(jid))
|
|
check_fields("GET /jobs/public/{id} 字段一致", data_of(body) or {})
|
|
st, body = http(base, "GET", api + "/companies/%s/jobs" % cid)
|
|
check_fields("GET /companies/{id}/jobs 字段一致", (data_of(body) or {}).get("items", [{}])[0])
|
|
st, body = http(base, "GET", api + "/companies/me/jobs", token=tok_a)
|
|
check_fields("GET /companies/me/jobs 字段一致", (data_of(body) or {}).get("items", [{}])[0])
|
|
st, body = http(base, "GET", api + "/jobs/" + str(jid), token=tok_a)
|
|
check_fields("GET /jobs/{id} 字段一致", data_of(body) or {})
|
|
st, body = http(base, "PUT", api + "/jobs/" + str(jid), token=tok_a, body={"title": "黑盒岗位-更新"})
|
|
check_fields("PUT /jobs/{id} 响应字段一致", data_of(body) or {})
|
|
|
|
# ── C. 缺省 / null / 空白 / benefits=[] ─────────────────────
|
|
st, body = http(base, "POST", api + "/jobs", token=tok_a, body={
|
|
"title": "最小岗位", "description": "d", "city": city, "employmentType": "FULL_TIME"})
|
|
d2 = data_of(body) or {}
|
|
record("C", "缺省新字段读为 null/[]", "all None + []",
|
|
"exp=%s sm=%s ben=%s" % (d2.get("experience"), d2.get("salaryMonths"), d2.get("benefits")),
|
|
d2.get("experience") is None and d2.get("salaryMonths") is None and d2.get("benefits") == [])
|
|
st, body = http(base, "PUT", api + "/jobs/%s" % jid, token=tok_a, body={"benefits": []})
|
|
record("C", "benefits=[] 清空", "[]", (data_of(body) or {}).get("benefits"),
|
|
(data_of(body) or {}).get("benefits") == [])
|
|
st, body = http(base, "PUT", api + "/jobs/%s" % jid, token=tok_a,
|
|
body={"experience": " 5-10年 ", "workLocation": " "})
|
|
d3 = data_of(body) or {}
|
|
record("C", "空白清空 workLocation / trim experience", "5-10年 / None",
|
|
"%s / %s" % (d3.get("experience"), d3.get("workLocation")),
|
|
d3.get("experience") == "5-10年" and d3.get("workLocation") is None)
|
|
|
|
# ── D. salaryMonths 类型与范围 ──────────────────────────────
|
|
base_job = {"title": "月数岗位", "description": "d", "city": city, "employmentType": "FULL_TIME"}
|
|
for raw in ["1", "24"]:
|
|
payload = json.dumps({**dict(base_job, title="月数OK" + raw), "salaryMonths": int(raw)}).encode()
|
|
st, body = http(base, "POST", api + "/jobs", token=tok_a, raw=payload)
|
|
record("D", "salaryMonths=%s 成功" % raw, 200, st, st == 200)
|
|
for raw in ["0", "-1", "25", "1.9", "24.9", "1.0", '"12"', '""', "true", "[]", "{}", "99999999999"]:
|
|
body = ('{"title":"月数非法","description":"d","city":"%s","employmentType":"FULL_TIME",'
|
|
'"salaryMonths":%s}' % (city, raw)).encode()
|
|
st, resp = http(base, "POST", api + "/jobs", token=tok_a, raw=body)
|
|
record("D", "salaryMonths=%s -> 400" % raw, 400, st, st == 400)
|
|
|
|
# ── E. 超长/非法 benefits -> 400 且无部分更新 ────────────────
|
|
st, _ = http(base, "POST", api + "/jobs", token=tok_a,
|
|
body=dict(base_job, title="超长经验", experience="e" * 41))
|
|
record("E", "experience 41 字符 -> 400", 400, st, st == 400)
|
|
st, _ = http(base, "POST", api + "/jobs", token=tok_a,
|
|
body=dict(base_job, title="超多福利", benefits=["b%d" % i for i in range(21)]))
|
|
record("E", "benefits 21 项 -> 400", 400, st, st == 400)
|
|
|
|
before = int(scalar("SELECT count(*) FROM jobs WHERE title='B01PARTIAL'"))
|
|
st, _ = http(base, "POST", api + "/jobs", token=tok_a,
|
|
body=dict(base_job, title="B01PARTIAL", salaryMonths=25))
|
|
after = int(scalar("SELECT count(*) FROM jobs WHERE title='B01PARTIAL'"))
|
|
record("E", "失败创建无部分写入", 400, "before=%d after=%d" % (before, after),
|
|
st == 400 and after == before)
|
|
|
|
st, _ = http(base, "PUT", api + "/jobs/" + str(jid), token=tok_a,
|
|
body={"title": "B01PARTIAL_TITLE", "salaryMonths": 25})
|
|
title_now = scalar("SELECT title FROM jobs WHERE id=%s" % jid)
|
|
record("E", "失败更新无部分写入", 400, "%s / %s" % (st, title_now),
|
|
st == 400 and title_now == "黑盒岗位-更新")
|
|
|
|
# ── F. 公开企业摘要白名单 ───────────────────────────────────
|
|
st, body = http(base, "GET", api + "/companies/" + str(cid))
|
|
node = data_of(body) or {}
|
|
keys = sorted(node.keys())
|
|
record("F", "公开企业摘要 7 字段", 7, keys,
|
|
keys == sorted(["id", "name", "logoUrl", "description", "industry", "verified", "companySize"]))
|
|
raw = body
|
|
leak_keys = [k for k in ("ownerUserId", "address", "website", "phone", "email",
|
|
"materialsJson", "reviewerId", "decisionReason") if k in node]
|
|
record("F", "无 ownerUserId/地址/联系方式", "no leak", "裸响应含敏感字段=%s" % leak_keys,
|
|
not leak_keys and "ownerUserId" not in raw)
|
|
|
|
# ── G. 草稿不可公开 / 跨企业不可改 ──────────────────────────
|
|
st, body = http(base, "POST", api + "/jobs", token=tok_a,
|
|
body=dict(base_job, title="黑盒草稿"))
|
|
draft = (data_of(body) or {}).get("id")
|
|
st, _ = http(base, "GET", api + "/jobs/public/" + str(draft))
|
|
record("G", "草稿公开详情 404", 404, st, st == 404)
|
|
tok_b, _ = as_company(base, phone(seq + 1))
|
|
st, _ = http(base, "PUT", api + "/jobs/%s" % jid, token=tok_b, body={"title": "越权"})
|
|
record("G", "跨企业修改 404", 404, st, st == 404)
|
|
|
|
# ── H. 历史 NULL 记录读取 ───────────────────────────────────
|
|
mysql("INSERT INTO jobs (company_id,title,description,city,employment_type,status,created_at,updated_at) "
|
|
"VALUES (%s,'B01LEGACY','desc','%s','FULL_TIME','PUBLISHED',NOW(),NOW());" % (cid, city))
|
|
legacy = scalar("SELECT id FROM jobs WHERE title='B01LEGACY'")
|
|
st, body = http(base, "GET", api + "/jobs/public/" + str(legacy))
|
|
node = data_of(body) or {}
|
|
record("H", "历史记录 benefits=[] 且新字段 null", "[] / None",
|
|
"%s / %s" % (node.get("benefits"), node.get("experience")),
|
|
node.get("benefits") == [] and node.get("experience") is None)
|
|
|
|
# ── J. 福利限制必须在 trim、去重之后判断(真实 HTTP) ──────
|
|
padded = " " * 25 + "A" * 40 + " " * 25
|
|
st, body = http(base, "POST", api + "/jobs", token=tok_a,
|
|
body=dict(base_job, title="福利归一化", benefits=[padded] * 21))
|
|
normal = data_of(body) or {}
|
|
normal_id = normal.get("id")
|
|
record("J", "创建允许原始超20项及trim前超40字", ["A" * 40], normal.get("benefits"),
|
|
st == 200 and normal.get("benefits") == ["A" * 40])
|
|
st, body = http(base, "PUT", api + "/jobs/" + str(normal_id), token=tok_a,
|
|
body={"benefits": [padded] * 21})
|
|
record("J", "更新同样按归一化后限制", ["A" * 40], (data_of(body) or {}).get("benefits"),
|
|
st == 200 and (data_of(body) or {}).get("benefits") == ["A" * 40])
|
|
st, _ = http(base, "PUT", api + "/jobs/" + str(normal_id), token=tok_a,
|
|
body={"benefits": ["A" * 41]})
|
|
record("J", "trim后41字拒绝", 400, st, st == 400)
|
|
twenty = ["B" + str(i) for i in range(20)]
|
|
st, body = http(base, "PUT", api + "/jobs/" + str(normal_id), token=tok_a, body={"benefits": twenty})
|
|
record("J", "20项不同福利允许", twenty, (data_of(body) or {}).get("benefits"),
|
|
st == 200 and (data_of(body) or {}).get("benefits") == twenty)
|
|
st, _ = http(base, "PUT", api + "/jobs/" + str(normal_id), token=tok_a,
|
|
body={"title": "不可部分更新", "benefits": twenty + ["B20"]})
|
|
record("J", "21项不同福利拒绝", 400, st, st == 400)
|
|
st, body = http(base, "GET", api + "/jobs/" + str(normal_id), token=tok_a)
|
|
preserved = data_of(body) or {}
|
|
record("J", "拒绝后无部分更新", "原题及20项福利", preserved.get("title"),
|
|
st == 200 and preserved.get("title") == "福利归一化" and preserved.get("benefits") == twenty)
|
|
st, body = http(base, "PUT", api + "/jobs/" + str(normal_id), token=tok_a, body={"benefits": None})
|
|
record("J", "null保留福利", twenty, (data_of(body) or {}).get("benefits"),
|
|
st == 200 and (data_of(body) or {}).get("benefits") == twenty)
|
|
st, body = http(base, "PUT", api + "/jobs/" + str(normal_id), token=tok_a, body={"benefits": []})
|
|
record("J", "空数组清空福利", [], (data_of(body) or {}).get("benefits"),
|
|
st == 200 and (data_of(body) or {}).get("benefits") == [])
|
|
|
|
# ── 持久化用夹具(供重启后核对) ───────────────────────────
|
|
mysql("UPDATE companies SET description='B01PERSIST' WHERE id=%s" % cid)
|
|
return {"companyId": cid, "jobId": jid, "city": city, "token": tok_a, "uid": uid_a}
|
|
|
|
|
|
def persist_create(base, api):
|
|
seq = int(time.time()) % 100000
|
|
tok, uid = as_company(base, phone(seq))
|
|
st, body = http(base, "POST", api + "/companies/me", token=tok,
|
|
body={"name": "B01PERSIST企业", "companySize": "500-999人",
|
|
"description": "B01PERSIST"})
|
|
cid = (data_of(body) or {}).get("id")
|
|
mysql("UPDATE companies SET verified=1 WHERE id=%s" % cid)
|
|
st, body = http(base, "POST", api + "/jobs", token=tok, body={
|
|
"title": "B01PERSIST岗位", "description": "d", "city": "B01PERSISTCITY",
|
|
"employmentType": "FULL_TIME", "experience": "5-10年", "salaryMonths": 16,
|
|
"benefits": ["五险一金"], "workLocation": "上海市"})
|
|
jid = (data_of(body) or {}).get("id")
|
|
print("PERSIST_CREATED companyId=%s jobId=%s" % (cid, jid))
|
|
return cid, jid
|
|
|
|
|
|
def persist_verify(base, api):
|
|
cid = scalar("SELECT id FROM companies WHERE name='B01PERSIST企业'")
|
|
jid = scalar("SELECT id FROM jobs WHERE title='B01PERSIST岗位'")
|
|
st, body = http(base, "GET", api + "/companies/" + str(cid))
|
|
record("I", "重启后企业资料仍在", "B01PERSIST企业/500-999人",
|
|
"%s/%s" % ((data_of(body) or {}).get("name"), (data_of(body) or {}).get("companySize")),
|
|
(data_of(body) or {}).get("name") == "B01PERSIST企业"
|
|
and (data_of(body) or {}).get("companySize") == "500-999人")
|
|
mysql("UPDATE jobs SET status='PUBLISHED', published_at=NOW() WHERE id=%s" % jid)
|
|
st, body = http(base, "GET", api + "/jobs/public/" + str(jid))
|
|
node = data_of(body) or {}
|
|
record("I", "重启后岗位展示字段仍在", "5-10年/16/[五险一金]",
|
|
"%s/%s/%s" % (node.get("experience"), node.get("salaryMonths"), node.get("benefits")),
|
|
node.get("experience") == "5-10年" and node.get("salaryMonths") == 16
|
|
and node.get("benefits") == ["五险一金"])
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--base", default="http://127.0.0.1:18080")
|
|
parser.add_argument("--api", default="/api/v1")
|
|
parser.add_argument("--mode", default="full",
|
|
choices=["full", "persist-create", "persist-verify"])
|
|
parser.add_argument("--out")
|
|
args = parser.parse_args()
|
|
global API
|
|
API = args.api
|
|
base = args.base.rstrip("/")
|
|
if args.mode == "persist-create":
|
|
persist_create(base, args.api)
|
|
return
|
|
if args.mode == "persist-verify":
|
|
persist_verify(base, args.api)
|
|
else:
|
|
summary = run(base, args.api)
|
|
print("FIXTURE companyId=%s jobId=%s" % (summary["companyId"], summary["jobId"]))
|
|
bad = [r for r in rows if not r["ok"]]
|
|
print("\n== BLACKBOX SUMMARY: total=%d fail=%d ==" % (len(rows), len(bad)))
|
|
if args.out:
|
|
with open(args.out, "w", encoding="utf-8") as fh:
|
|
json.dump(rows, fh, ensure_ascii=False, indent=1)
|
|
sys.exit(1 if bad else 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|