15 changed files with 492 additions and 29 deletions
@ -0,0 +1,37 @@ |
|||||
|
--- |
||||
|
task_id: "5" |
||||
|
task_hash: "sha256:3d4c3ad3efc8a298211c74dd6c8da2d95fe4a8c1269fc58f0672ccc5f148e3c6" |
||||
|
next: "Task 6: history list/detail/delete" |
||||
|
completed: "Calculation save, policy version and idempotency" |
||||
|
evidence: "W3 suite 6/6 passed" |
||||
|
review: ".superpowers/sdd/reviews/T5.md" |
||||
|
risk: "Concurrent same-key race needs MySQL validation" |
||||
|
commit_start: "4b42a624d7ec2bb5cac64bc13cd7d4d8eba91ee7" |
||||
|
commit_end: "27dbc095e0413f9566bcc4ddc3bdb4f642535a6b" |
||||
|
created_at: "2026-09-20T08:34:50.804Z" |
||||
|
plan_hash: "sha256:77341d6e000ef1624d59d66b8b361c24e7ef8f1b5d065dd4d648a2a7537e72d5" |
||||
|
plan_revision: 1 |
||||
|
--- |
||||
|
|
||||
|
# Checkpoint: 5 |
||||
|
|
||||
|
## Next |
||||
|
Task 6: history list/detail/delete |
||||
|
|
||||
|
## Completed |
||||
|
Calculation save, policy version and idempotency |
||||
|
|
||||
|
## Evidence |
||||
|
W3 suite 6/6 passed |
||||
|
|
||||
|
## Review |
||||
|
.superpowers/sdd/reviews/T5.md |
||||
|
|
||||
|
## Risk |
||||
|
Concurrent same-key race needs MySQL validation |
||||
|
|
||||
|
## Commit Start |
||||
|
4b42a624d7ec2bb5cac64bc13cd7d4d8eba91ee7 |
||||
|
|
||||
|
## Commit End |
||||
|
27dbc095e0413f9566bcc4ddc3bdb4f642535a6b |
||||
@ -0,0 +1,37 @@ |
|||||
|
--- |
||||
|
task_id: "6" |
||||
|
task_hash: "sha256:ee793f06e3bc8e83ed5836909b204f7234a68754d4ce938ae9fd927be426c7e6" |
||||
|
next: "Task 7: authorization, logging privacy and account deletion cleanup" |
||||
|
completed: "History list/detail/delete and snapshot responses" |
||||
|
evidence: "W3 suite 6/6 passed" |
||||
|
review: ".superpowers/sdd/reviews/T6.md" |
||||
|
risk: "Snapshot JSON compatibility requires MySQL validation" |
||||
|
commit_start: "4b42a624d7ec2bb5cac64bc13cd7d4d8eba91ee7" |
||||
|
commit_end: "27dbc095e0413f9566bcc4ddc3bdb4f642535a6b" |
||||
|
created_at: "2026-09-20T08:34:50.905Z" |
||||
|
plan_hash: "sha256:77341d6e000ef1624d59d66b8b361c24e7ef8f1b5d065dd4d648a2a7537e72d5" |
||||
|
plan_revision: 1 |
||||
|
--- |
||||
|
|
||||
|
# Checkpoint: 6 |
||||
|
|
||||
|
## Next |
||||
|
Task 7: authorization, logging privacy and account deletion cleanup |
||||
|
|
||||
|
## Completed |
||||
|
History list/detail/delete and snapshot responses |
||||
|
|
||||
|
## Evidence |
||||
|
W3 suite 6/6 passed |
||||
|
|
||||
|
## Review |
||||
|
.superpowers/sdd/reviews/T6.md |
||||
|
|
||||
|
## Risk |
||||
|
Snapshot JSON compatibility requires MySQL validation |
||||
|
|
||||
|
## Commit Start |
||||
|
4b42a624d7ec2bb5cac64bc13cd7d4d8eba91ee7 |
||||
|
|
||||
|
## Commit End |
||||
|
27dbc095e0413f9566bcc4ddc3bdb4f642535a6b |
||||
@ -0,0 +1,37 @@ |
|||||
|
--- |
||||
|
task_id: "7" |
||||
|
task_hash: "sha256:a3bf97aab3db413f757f95db0c0698d2a2041fe87309be003ccff6f2a267f162" |
||||
|
next: "Wave W4 task 8: full module tests, OpenAPI and docs" |
||||
|
completed: "Authorization, privacy boundaries and account deletion purge" |
||||
|
evidence: "W3 suite 6/6 passed" |
||||
|
review: ".superpowers/sdd/reviews/T7.md" |
||||
|
risk: "Legacy deletion tests must remain green after constructor change" |
||||
|
commit_start: "4b42a624d7ec2bb5cac64bc13cd7d4d8eba91ee7" |
||||
|
commit_end: "27dbc095e0413f9566bcc4ddc3bdb4f642535a6b" |
||||
|
created_at: "2026-09-20T08:34:51.016Z" |
||||
|
plan_hash: "sha256:77341d6e000ef1624d59d66b8b361c24e7ef8f1b5d065dd4d648a2a7537e72d5" |
||||
|
plan_revision: 1 |
||||
|
--- |
||||
|
|
||||
|
# Checkpoint: 7 |
||||
|
|
||||
|
## Next |
||||
|
Wave W4 task 8: full module tests, OpenAPI and docs |
||||
|
|
||||
|
## Completed |
||||
|
Authorization, privacy boundaries and account deletion purge |
||||
|
|
||||
|
## Evidence |
||||
|
W3 suite 6/6 passed |
||||
|
|
||||
|
## Review |
||||
|
.superpowers/sdd/reviews/T7.md |
||||
|
|
||||
|
## Risk |
||||
|
Legacy deletion tests must remain green after constructor change |
||||
|
|
||||
|
## Commit Start |
||||
|
4b42a624d7ec2bb5cac64bc13cd7d4d8eba91ee7 |
||||
|
|
||||
|
## Commit End |
||||
|
27dbc095e0413f9566bcc4ddc3bdb4f642535a6b |
||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"status": "pass", |
||||
|
"base": "4b42a624d7ec2bb5cac64bc13cd7d4d8eba91ee7", |
||||
|
"head": "27dbc095e0413f9566bcc4ddc3bdb4f642535a6b", |
||||
|
"report": ".superpowers\\sdd\\reviews\\W3.md", |
||||
|
"report_sha256": "sha256:7d2823941f43fd101c1271be77703e6ae614650c64c7c1fe1484a7cae41f68fa", |
||||
|
"plan_hash": "sha256:77341d6e000ef1624d59d66b8b361c24e7ef8f1b5d065dd4d648a2a7537e72d5", |
||||
|
"plan_revision": 1, |
||||
|
"recorded_at": "2026-09-20T08:34:51.607Z" |
||||
|
} |
||||
@ -1,14 +1,10 @@ |
|||||
# n05-salary-calc 执行进度 |
# n05-salary-calc 执行进度 |
||||
|
|
||||
## W1 - 数据基础:完成 |
## W1-W3 |
||||
- V40、实体、Repository、359 城分层 V41、迁移和种子测试;3/3 通过。 |
- 全部完成并已有 pass review receipt。 |
||||
- W1 review receipt:pass。 |
|
||||
|
|
||||
## W2 - 城市查询与计算引擎:完成 |
## W4 - 发布证明与文档 |
||||
- 城市列表/详情、STUDENT 权限、dataLevel 和 BigDecimal 计算引擎;15/15 通过。 |
- Task 8:完成。薪资专项 24/24,OpenAPI 139 paths / 178 operations,API/联调/路线图文档已更新。 |
||||
- W2 review receipt:pass。 |
- Task 9:阻塞。完整 `mvn clean verify` 运行 1516 tests,仍有既有 2 failures / 9 errors,且失败不位于 salarycalculator。 |
||||
|
- Task 9 Docker:本机无 `docker`、`podman` 或 `nerdctl` CLI/服务,WSL 访问被拒绝;未使用 3307 常驻实例执行任何迁移或黑盒写入。 |
||||
## W3 - 历史、幂等、权限与注销:完成待 review |
- 一次性容器专用脚本:`scripts/verify_salary_calculator_blackbox.py`,待 Docker 可用后运行。 |
||||
- 计算并保存、政策版本冲突、幂等重放/冲突、历史列表/详情/物理删除、跨用户隔离和账号注销清理。 |
|
||||
- 验证:`mvn -Dtest=SalaryAuthorizationIntegrationTest,SalaryCalculationEndToEndIntegrationTest,SalaryAccountDeletionIntegrationTest test`,6/6 通过。 |
|
||||
- 隐私:薪资包无直接日志写入;API 不返回 requestHash、idempotencyKey 或 studentUserId。 |
|
||||
|
|||||
@ -0,0 +1,10 @@ |
|||||
|
{ |
||||
|
"status": "pass", |
||||
|
"base": "4b42a624d7ec2bb5cac64bc13cd7d4d8eba91ee7", |
||||
|
"head": "27dbc095e0413f9566bcc4ddc3bdb4f642535a6b", |
||||
|
"report": ".superpowers\\sdd\\reviews\\W3.md", |
||||
|
"report_sha256": "sha256:7d2823941f43fd101c1271be77703e6ae614650c64c7c1fe1484a7cae41f68fa", |
||||
|
"plan_hash": "sha256:77341d6e000ef1624d59d66b8b361c24e7ef8f1b5d065dd4d648a2a7537e72d5", |
||||
|
"plan_revision": 1, |
||||
|
"recorded_at": "2026-09-20T08:34:51.607Z" |
||||
|
} |
||||
@ -0,0 +1,7 @@ |
|||||
|
# Wave W4 blocked |
||||
|
|
||||
|
- Task 8: pass evidence recorded in progress ledger. |
||||
|
- Task 9: blocked by missing Docker runtime. |
||||
|
- Docker check: `docker`, `podman`, and `nerdctl` are absent from PATH and standard install paths; Docker Desktop service/files not found; WSL invocation is access denied. |
||||
|
- Policy: the resident MySQL at 127.0.0.1:3307 is prohibited for W4 acceptance and was not migrated or written to. |
||||
|
- Resume command: install/enable Docker, start app against a disposable MySQL container, then run `python scripts/verify_salary_calculator_blackbox.py` with explicit black-box environment variables. |
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,158 @@ |
|||||
|
# 薪资计算器 API(N05) |
||||
|
|
||||
|
> Base URL:`/api/v1` |
||||
|
> 认证:`Authorization: Bearer <access-token>` |
||||
|
> 角色:当前登录用户必须具备 `STUDENT` 角色。 |
||||
|
|
||||
|
## 1. 能力边界 |
||||
|
|
||||
|
- 输入税前月薪,估算个人五险一金、大病固定费和当月个税后的到手工资。 |
||||
|
- 采用居民个人综合所得累计预扣法;`monthIndex` 表示当前工资口径下的累计计税月份数,而不是自然月编号。 |
||||
|
- 支持用户覆盖社保基数、公积金基数和个人比例。 |
||||
|
- 城市政策以地级行政区、直辖市和省直辖县级市为最小单元,不拆分到区县。 |
||||
|
- `dataLevel=CITY` 表示城市级参数;`dataLevel=PROVINCE_FALLBACK` 表示由于公开城市级资料不足而使用省级回退参数,结果精度较低。 |
||||
|
- 所有结果均为估算值,实际以工资单、当地政策和税务申报为准。 |
||||
|
|
||||
|
## 2. 查询城市列表 |
||||
|
|
||||
|
`GET /salary-calculator/cities` |
||||
|
|
||||
|
返回全部启用城市摘要,按省份编码和城市编码排序。 |
||||
|
|
||||
|
响应示例: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"code": 200, |
||||
|
"message": "ok", |
||||
|
"data": [ |
||||
|
{ |
||||
|
"cityCode": "110000", |
||||
|
"provinceCode": "110000", |
||||
|
"provinceName": "北京市", |
||||
|
"cityName": "北京市", |
||||
|
"policyVersion": "2026-09-20-v1", |
||||
|
"policyEffectiveDate": "2026-07-01", |
||||
|
"dataLevel": "CITY" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## 3. 查询城市默认参数 |
||||
|
|
||||
|
`GET /salary-calculator/cities/{cityCode}` |
||||
|
|
||||
|
返回城市级或省级回退的当前默认参数。计算请求必须原样携带响应中的 `policyVersion` 作为 `expectedPolicyVersion`。 |
||||
|
|
||||
|
## 4. 计算并保存 |
||||
|
|
||||
|
`POST /salary-calculator/calculations` |
||||
|
|
||||
|
请求示例: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"cityCode": "110000", |
||||
|
"grossMonthlySalary": 10000.00, |
||||
|
"monthIndex": 1, |
||||
|
"specialAdditionalDeduction": 0.00, |
||||
|
"expectedPolicyVersion": "2026-09-20-v1", |
||||
|
"idempotencyKey": "optional-client-key", |
||||
|
"overrides": { |
||||
|
"socialSecurityBase": 10000.00, |
||||
|
"housingFundBase": 10000.00, |
||||
|
"pensionRate": 0.08, |
||||
|
"medicalRate": 0.02, |
||||
|
"unemploymentRate": 0.005, |
||||
|
"housingFundRate": 0.12, |
||||
|
"seriousIllnessMonthlyFee": 0.00 |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
- `cityCode`、`grossMonthlySalary`、`expectedPolicyVersion` 必填。 |
||||
|
- `monthIndex` 默认 1,范围 1–12。 |
||||
|
- `specialAdditionalDeduction` 默认 0。 |
||||
|
- `idempotencyKey` 可选,最长 64 字符;同用户同键同规范化请求返回原历史,同键不同请求返回 `409/9804`。 |
||||
|
- 覆盖基数必须位于对应城市范围内;比例为 0 时允许基数为 0。 |
||||
|
|
||||
|
成功响应: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"code": 200, |
||||
|
"message": "ok", |
||||
|
"data": { |
||||
|
"historyId": 123, |
||||
|
"createdAt": "2026-09-20T16:00:00", |
||||
|
"result": { |
||||
|
"cityCode": "110000", |
||||
|
"cityName": "北京市", |
||||
|
"dataLevel": "CITY", |
||||
|
"policyVersion": "2026-09-20-v1", |
||||
|
"policyEffectiveDate": "2026-07-01", |
||||
|
"taxRulesVersion": "CN_IIT_2019_V1", |
||||
|
"estimate": true, |
||||
|
"monthIndex": 1, |
||||
|
"grossMonthlySalary": 10000.00, |
||||
|
"socialSecurityBase": 10000.00, |
||||
|
"housingFundBase": 10000.00, |
||||
|
"pension": 800.00, |
||||
|
"medical": 200.00, |
||||
|
"unemployment": 50.00, |
||||
|
"housingFund": 1200.00, |
||||
|
"currentMonthTax": 82.50, |
||||
|
"netSalary": 7667.50 |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## 5. 查询历史 |
||||
|
|
||||
|
`GET /salary-calculator/calculations?page=0&size=20` |
||||
|
|
||||
|
- 仅返回当前用户的记录。 |
||||
|
- 按创建时间倒序、历史 ID 倒序。 |
||||
|
- `size` 最大 100。 |
||||
|
|
||||
|
`GET /salary-calculator/calculations/{id}` |
||||
|
|
||||
|
返回输入、政策、覆盖项和结果三组完整 JSON 快照。访问他人历史统一返回 `404/9803`。 |
||||
|
|
||||
|
## 6. 删除历史 |
||||
|
|
||||
|
`DELETE /salary-calculator/calculations/{id}` |
||||
|
|
||||
|
物理删除本人历史;成功后再次查询返回 `404/9803`。删除后原幂等键可重新使用。 |
||||
|
|
||||
|
## 7. 错误码 |
||||
|
|
||||
|
| HTTP | code | 含义 | |
||||
|
|---|---|---| |
||||
|
| 400 | 9802 | 工资、比例、基数或分页参数非法 | |
||||
|
| 401 | 401 | 未登录或 Token 无效 | |
||||
|
| 403 | 2002 | 已登录但不是 `STUDENT` | |
||||
|
| 404 | 9801 | 城市不存在、停用或未配置 | |
||||
|
| 404 | 9803 | 历史不存在或不属于当前用户 | |
||||
|
| 409 | 9804 | 幂等键冲突 | |
||||
|
| 409 | 9805 | 城市政策版本已变化 | |
||||
|
|
||||
|
`9805` 响应会在 `data` 中返回最新 `policyVersion` 和 `policyEffectiveDate`,客户端应重新加载城市详情后再试算。 |
||||
|
|
||||
|
## 8. 计算口径 |
||||
|
|
||||
|
```text |
||||
|
社保基数 = 用户覆盖值,否则 min(max(税前月薪, 社保下限), 社保上限) |
||||
|
公积金基数 = 用户覆盖值,否则 min(max(税前月薪, 公积金下限), 公积金上限) |
||||
|
|
||||
|
累计收入 = 税前月薪 × monthIndex |
||||
|
累计基本减除费用 = 5000 × monthIndex |
||||
|
累计专项附加扣除 = 月度专项附加扣除 × monthIndex |
||||
|
累计应纳税所得额 = max(累计收入 - 累计个人社保 - 累计公积金 - 累计大病费 |
||||
|
- 累计基本减除费用 - 累计专项附加扣除, 0) |
||||
|
当月个税 = max(累计应纳税额 - 累计已预扣税额, 0) |
||||
|
到手工资 = max(税前月薪 - 个人养老 - 个人医疗 - 个人失业 - 个人公积金 |
||||
|
- 大病固定费 - 当月个税, 0) |
||||
|
``` |
||||
@ -0,0 +1,131 @@ |
|||||
|
"""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 str(first.json()["data"]["result"]["netSalary"]) == "7667.50", 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 |
||||
Loading…
Reference in new issue