轻松聘AI开发
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.
 
 
 

131 lines
5.2 KiB

"""N05 salary calculator black-box verification.
Run only against an isolated, disposable MySQL container and an app wired to it.
Required environment variables:
BASE_URL=http://127.0.0.1:<PORT>/api/v1
DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_NAME
"""
import os
import sys
import time
import pymysql
import requests
BASE = os.environ.get("BASE_URL", "").rstrip("/")
DB = {
"host": os.environ["DB_HOST"],
"port": int(os.environ["DB_PORT"]),
"user": os.environ["DB_USERNAME"],
"password": os.environ["DB_PASSWORD"],
"database": os.environ["DB_NAME"],
}
if not BASE:
raise SystemExit("BASE_URL is required")
def db():
return pymysql.connect(**DB, autocommit=True)
def sql_one(query, args=()):
with db() as conn:
with conn.cursor() as cur:
cur.execute(query, args)
return cur.fetchone()
def sql_all(query, args=()):
with db() as conn:
with conn.cursor() as cur:
cur.execute(query, args)
return cur.fetchall()
def login_student(phone):
r = requests.post(f"{BASE}/auth/sms/send", params={"phone": phone, "purpose": "login"}, timeout=10)
assert r.status_code == 200, r.text
code = None
for _ in range(20):
row = sql_one(
"SELECT code FROM sms_verification_codes "
"WHERE phone=%s AND purpose='login' AND used=0 ORDER BY id DESC LIMIT 1",
(phone,),
)
if row:
code = row[0]
break
time.sleep(0.25)
assert code, "mock SMS code not found"
r = requests.post(f"{BASE}/auth/login", json={"phone": phone, "code": code, "purpose": "login"}, timeout=10)
assert r.status_code == 200, r.text
return r.json()["data"]["accessToken"]
def main():
versions = {row[0] for row in sql_all("SELECT version FROM flyway_schema_history")}
assert "40" in versions and "41" in versions, versions
assert sql_one("SELECT COUNT(*) FROM salary_city_policy")[0] == 359
levels = dict(sql_all("SELECT data_level, COUNT(*) FROM salary_city_policy GROUP BY data_level"))
assert levels == {"CITY": 12, "PROVINCE_FALLBACK": 347}, levels
phone = os.environ.get("BLACKBOX_PHONE", f"139{int(time.time()) % 100000000:08d}")
token = login_student(phone)
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
cities = requests.get(f"{BASE}/salary-calculator/cities", headers=headers, timeout=10)
assert cities.status_code == 200 and len(cities.json()["data"]) == 359, cities.text
city = requests.get(f"{BASE}/salary-calculator/cities/110000", headers=headers, timeout=10)
assert city.status_code == 200 and city.json()["data"]["dataLevel"] == "CITY", city.text
payload = {
"cityCode": "110000",
"grossMonthlySalary": 10000.00,
"monthIndex": 1,
"specialAdditionalDeduction": 0.00,
"expectedPolicyVersion": city.json()["data"]["policyVersion"],
"idempotencyKey": f"blackbox-{phone}",
"overrides": None,
}
first = requests.post(f"{BASE}/salary-calculator/calculations", headers=headers, json=payload, timeout=10)
assert first.status_code == 200, first.text
history_id = first.json()["data"]["historyId"]
assert float(first.json()["data"]["result"]["netSalary"]) == 7667.5, first.text
replay = requests.post(f"{BASE}/salary-calculator/calculations", headers=headers, json=payload, timeout=10)
assert replay.status_code == 200 and replay.json()["data"]["historyId"] == history_id, replay.text
conflict_payload = dict(payload)
conflict_payload["grossMonthlySalary"] = 12000.00
conflict = requests.post(f"{BASE}/salary-calculator/calculations", headers=headers, json=conflict_payload, timeout=10)
assert conflict.status_code == 409 and conflict.json()["code"] == 9804, conflict.text
stale_payload = dict(payload)
stale_payload["idempotencyKey"] = "stale-" + phone
stale_payload["expectedPolicyVersion"] = "stale-version"
stale = requests.post(f"{BASE}/salary-calculator/calculations", headers=headers, json=stale_payload, timeout=10)
assert stale.status_code == 409 and stale.json()["code"] == 9805, stale.text
assert stale.json()["data"]["latestPolicyVersion"] == city.json()["data"]["policyVersion"]
history = requests.get(f"{BASE}/salary-calculator/calculations", headers=headers, timeout=10)
assert history.status_code == 200 and history.json()["data"]["total"] == 1, history.text
detail = requests.get(f"{BASE}/salary-calculator/calculations/{history_id}", headers=headers, timeout=10)
assert detail.status_code == 200 and detail.json()["data"]["policySnapshot"]["dataLevel"] == "CITY", detail.text
deleted = requests.delete(f"{BASE}/salary-calculator/calculations/{history_id}", headers=headers, timeout=10)
assert deleted.status_code == 200, deleted.text
missing = requests.get(f"{BASE}/salary-calculator/calculations/{history_id}", headers=headers, timeout=10)
assert missing.status_code == 404 and missing.json()["code"] == 9803, missing.text
print("PASS: N05 salary calculator disposable-MySQL black box")
if __name__ == "__main__":
try:
main()
except Exception as exc:
print(f"FAIL: {type(exc).__name__}: {exc}", file=sys.stderr)
raise