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

#!/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())