forked from baskbuddy/zhangshun
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.
140 lines
5.5 KiB
140 lines
5.5 KiB
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""本批 OpenAPI 契约对照脚本的**反向测试**。
|
|
|
|
通过在**临时副本**中分别制造下列变异,证明严格对照脚本在每种情况下都会失败
|
|
(进程退出码非 0);**不修改正式契约**来制造这些测试:
|
|
|
|
1. 删除一个约束(JobCreateRequest.experience.maxLength)
|
|
2. 改错类型(JobCreateRequest.salaryMonths.type: integer → string)
|
|
3. 改错范围(JobCreateRequest.salaryMonths.maximum: 24 → 12)
|
|
4. 移除字段(JobCreateRequest.properties.education)
|
|
5. 加入敏感字段(CompanyPublicView.properties.ownerUserId)
|
|
6. 改错可空性(JobCreateRequest.experience.type 去掉 "null")
|
|
7. 改错数组元素长度(benefits.items.maxLength: 40 → 41)
|
|
8. 改错响应引用(/jobs/public/{jobId} 200 引用改为 ApiResponseJobDetailViewDoc)
|
|
|
|
用法:
|
|
python b01_contract_negative_tests.py [--runtime PATH] [--static PATH]
|
|
退出码:0=全部(基线通过 + 各变异均失败);1=存在不符合预期的结果。
|
|
"""
|
|
import argparse
|
|
import copy
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
ROOT = HERE.parents[2]
|
|
COMPARE = HERE / "b01_openapi_compare.py"
|
|
DEFAULT_STATIC = ROOT / "docs" / "openapi-rc.json"
|
|
DEFAULT_RUNTIME = HERE / "runtime-openapi.json"
|
|
|
|
results = []
|
|
|
|
|
|
def run_compare(static_path, runtime_path, out_path):
|
|
proc = subprocess.run(
|
|
[sys.executable, str(COMPARE), "--static", str(static_path),
|
|
"--runtime", str(runtime_path), "--out", str(out_path),
|
|
"--runtime-out", str(out_path)],
|
|
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
|
return proc.returncode, proc.stdout
|
|
|
|
|
|
def mutate_remove_max_length(doc):
|
|
doc["components"]["schemas"]["JobCreateRequest"]["properties"]["experience"].pop("maxLength", None)
|
|
|
|
|
|
def mutate_wrong_type(doc):
|
|
doc["components"]["schemas"]["JobCreateRequest"]["properties"]["salaryMonths"]["type"] = ["string", "null"]
|
|
|
|
|
|
def mutate_wrong_range(doc):
|
|
doc["components"]["schemas"]["JobCreateRequest"]["properties"]["salaryMonths"]["maximum"] = 12
|
|
|
|
|
|
def mutate_remove_field(doc):
|
|
doc["components"]["schemas"]["JobCreateRequest"]["properties"].pop("education", None)
|
|
|
|
|
|
def mutate_sensitive_field(doc):
|
|
doc["components"]["schemas"]["CompanyPublicView"]["properties"]["ownerUserId"] = {"type": "integer"}
|
|
|
|
|
|
def mutate_nullability(doc):
|
|
doc["components"]["schemas"]["JobCreateRequest"]["properties"]["experience"]["type"] = "string"
|
|
|
|
|
|
def mutate_item_length(doc):
|
|
doc["components"]["schemas"]["JobCreateRequest"]["properties"]["benefits"]["x-normalization"]["itemMaxLength"] = 41
|
|
|
|
def mutate_normalization_removed(doc):
|
|
doc["components"]["schemas"]["JobCreateRequest"]["properties"]["benefits"].pop("x-normalization")
|
|
|
|
def mutate_trim(doc):
|
|
doc["components"]["schemas"]["JobCreateRequest"]["properties"]["benefits"]["x-normalization"]["trimItems"] = False
|
|
|
|
def mutate_raw_limit(doc):
|
|
doc["components"]["schemas"]["JobCreateRequest"]["properties"]["benefits"]["maxItems"] = 20
|
|
|
|
def mutate_benefits_null(doc):
|
|
doc["components"]["schemas"]["JobCreateRequest"]["properties"]["benefits"]["type"] = "array"
|
|
|
|
|
|
def mutate_response_ref(doc):
|
|
doc["paths"]["/jobs/public/{jobId}"]["get"]["responses"]["200"]["content"]["*/*"]["schema"]["$ref"] = \
|
|
"#/components/schemas/ApiResponseJobDetailViewDoc"
|
|
|
|
|
|
MUTATIONS = [
|
|
("① 删除约束 experience.maxLength", mutate_remove_max_length),
|
|
("② 改错类型 salaryMonths→string", mutate_wrong_type),
|
|
("③ 改错范围 salaryMonths.maximum=12", mutate_wrong_range),
|
|
("④ 移除字段 education", mutate_remove_field),
|
|
("⑤ 加入敏感字段 ownerUserId", mutate_sensitive_field),
|
|
("⑥ 改错可空性 experience 去掉 null", mutate_nullability),
|
|
("⑦ 改错数组元素长度 items.maxLength=41", mutate_item_length),
|
|
("⑧ 改错响应引用 public job", mutate_response_ref),
|
|
("⑨ 移除归一化规则", mutate_normalization_removed),
|
|
("⑩ 错误关闭trim", mutate_trim),
|
|
("⑪ 错误限制原始请求20项", mutate_raw_limit),
|
|
("⑫ 错误拒绝福利null", mutate_benefits_null),
|
|
]
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--static", default=str(DEFAULT_STATIC))
|
|
parser.add_argument("--runtime", default=str(DEFAULT_RUNTIME))
|
|
args = parser.parse_args()
|
|
|
|
baseline = json.load(open(args.static, encoding="utf-8"))
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
tmp = Path(tmp)
|
|
# 0) 基线(未变异)必须通过
|
|
out0 = tmp / "compare-baseline.json"
|
|
rc, _ = run_compare(args.static, args.runtime, out0)
|
|
results.append(("基线(正式契约 + 运行时)通过", rc == 0, "rc=%d" % rc))
|
|
|
|
# 各变异必须失败(rc != 0)
|
|
for name, mutate in MUTATIONS:
|
|
doc = copy.deepcopy(baseline)
|
|
mutate(doc)
|
|
mutated = tmp / "static-mutated.json"
|
|
mutated.write_text(json.dumps(doc, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
out = tmp / "compare.json"
|
|
rc, stdout = run_compare(mutated, args.runtime, out)
|
|
results.append(("%s → 对照脚本失败" % name, rc != 0, "rc=%d" % rc))
|
|
|
|
bad = [r for r in results if not r[1]]
|
|
for name, ok, detail in results:
|
|
print("[%s] %-46s %s" % ("PASS" if ok else "FAIL", name, detail))
|
|
print("\n== CONTRACT NEGATIVE SUMMARY: total=%d fail=%d ==" % (len(results), len(bad)))
|
|
return 1 if bad else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|
|
|