250 lines
8.0 KiB
Python
250 lines
8.0 KiB
Python
# 接口 1 收件契约。假连接录 SQL,不连 Oracle。
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
|
||
import pytest
|
||
from fastapi.testclient import TestClient
|
||
|
||
os.environ["OMS_INBOUND_TOKEN"] = "test-oms-token"
|
||
|
||
from app.main import app, get_pool
|
||
from tests.fake_oracle import FakePool, ora_error
|
||
|
||
TOKEN = "test-oms-token"
|
||
PATH = "/api/oms/v1/sales-goods"
|
||
|
||
|
||
# 构造一条合法的接口 1 明细,overrides 覆盖字段。
|
||
def _item(**overrides):
|
||
row = {
|
||
"omsId": 2026092801,
|
||
"barCode": "06901234567890",
|
||
"orderDate": "2026-09-28",
|
||
"salePrice": "128.5000",
|
||
"saleQty": 10,
|
||
"planPurchaseQty": 10,
|
||
"goodsId": "G000123456789",
|
||
"goodsName": "阿莫西林胶囊",
|
||
"goodsSpec": "0.25g*24粒",
|
||
"manufacturer": "示例制药有限公司",
|
||
"approvalNo": "国药准字H20201234",
|
||
"unit": "盒",
|
||
"customerId": "C00010001",
|
||
"customerName": "示例医院",
|
||
"orgCode": "907",
|
||
"goodsMdmCode": "QX0100167583",
|
||
"customerMdmCode": "DW0100079727",
|
||
}
|
||
row.update(overrides)
|
||
return row
|
||
|
||
|
||
# 注入 FakePool,整次请求走同一块假库。
|
||
@pytest.fixture
|
||
def client():
|
||
pool = FakePool()
|
||
app.dependency_overrides[get_pool] = lambda: pool
|
||
with TestClient(app) as test_client:
|
||
yield test_client, pool
|
||
app.dependency_overrides.clear()
|
||
|
||
|
||
# 带或不带 X-Oms-Token 发 POST。
|
||
def _post(client, body, token=TOKEN):
|
||
headers = {"X-Oms-Token": token} if token is not None else {}
|
||
return client.post(PATH, json=body, headers=headers)
|
||
|
||
|
||
# 缺 X-Oms-Token 时接口返回 401。
|
||
def test_missing_token_returns_401(client):
|
||
test_client, pool = client
|
||
resp = _post(test_client, {"data": [_item()]}, token=None)
|
||
assert resp.status_code == 401
|
||
body = resp.json()
|
||
assert body["code"] == 401
|
||
assert body["msg"] == "身份认证失败"
|
||
assert body["results"] == []
|
||
assert pool.db.audits
|
||
|
||
|
||
# 错误 token 时接口返回 401。
|
||
def test_wrong_token_returns_401(client):
|
||
test_client, _pool = client
|
||
resp = _post(test_client, {"data": [_item()]}, token="bad")
|
||
assert resp.status_code == 401
|
||
assert resp.json()["code"] == 401
|
||
|
||
|
||
# 缺 data 节点时整包 422,一条都不落。
|
||
def test_missing_data_returns_422(client):
|
||
test_client, pool = client
|
||
resp = _post(test_client, {})
|
||
assert resp.status_code == 422
|
||
assert resp.json()["code"] == 422
|
||
assert resp.json()["results"] == []
|
||
assert pool.db.sales == {}
|
||
|
||
|
||
# data 空数组时整包 422。
|
||
def test_empty_data_returns_422(client):
|
||
test_client, _pool = client
|
||
resp = _post(test_client, {"data": []})
|
||
assert resp.status_code == 422
|
||
assert resp.json()["code"] == 422
|
||
|
||
|
||
# 超过 100 条时整包 422,假库仍空。
|
||
def test_over_100_rows_returns_422(client):
|
||
test_client, pool = client
|
||
rows = [_item(omsId=i) for i in range(1, 102)]
|
||
resp = _post(test_client, {"data": rows})
|
||
assert resp.status_code == 422
|
||
assert pool.db.sales == {}
|
||
|
||
|
||
# 合法一行 ACCEPTED,deal_flag=0,条码和 MDM 按 snake_case 落库。
|
||
def test_accepts_valid_row(client):
|
||
test_client, pool = client
|
||
resp = _post(test_client, {"data": [_item()]})
|
||
assert resp.status_code == 200
|
||
body = resp.json()
|
||
assert body["code"] == 200
|
||
assert body["results"] == [
|
||
{
|
||
"omsId": 2026092801,
|
||
"resultCode": "ACCEPTED",
|
||
"message": "接收成功,等待RPA处理",
|
||
}
|
||
]
|
||
stored = pool.db.sales[2026092801]
|
||
assert stored["deal_flag"] == 0
|
||
assert stored["bar_code"] == "06901234567890"
|
||
assert stored["goods_mdm_code"] == "QX0100167583"
|
||
|
||
|
||
# 跨请求同 omsId:DUPLICATE,库里品名不改。
|
||
def test_existing_oms_id_is_duplicate_and_not_updated(client):
|
||
test_client, pool = client
|
||
_post(test_client, {"data": [_item()]})
|
||
resp = _post(test_client, {"data": [_item(goodsName="不应覆盖")]})
|
||
assert resp.status_code == 200
|
||
result = resp.json()["results"][0]
|
||
assert result["resultCode"] == "DUPLICATE"
|
||
assert pool.db.sales[2026092801]["goods_name"] == "阿莫西林胶囊"
|
||
|
||
|
||
# 同一 POST 里两条相同 omsId:后一条 CONFLICT,只落第一条。
|
||
def test_duplicate_oms_id_in_same_batch_is_conflict(client):
|
||
test_client, pool = client
|
||
resp = _post(
|
||
test_client,
|
||
{"data": [_item(), _item(barCode="06901234567891")]},
|
||
)
|
||
assert resp.status_code == 200
|
||
results = resp.json()["results"]
|
||
assert results[0]["resultCode"] == "ACCEPTED"
|
||
assert results[1]["resultCode"] == "CONFLICT"
|
||
assert results[1]["omsId"] == 2026092801
|
||
assert len(pool.db.sales) == 1
|
||
|
||
|
||
# 缺 MDM 码 REJECTED,不入库。
|
||
def test_missing_mdm_is_rejected_and_not_stored(client):
|
||
test_client, pool = client
|
||
resp = _post(test_client, {"data": [_item(goodsMdmCode="")]})
|
||
assert resp.status_code == 200
|
||
result = resp.json()["results"][0]
|
||
assert result["resultCode"] == "REJECTED"
|
||
assert "goodsMdmCode" in result["message"]
|
||
assert pool.db.sales == {}
|
||
|
||
|
||
# 混批 DUPLICATE + ACCEPTED + REJECTED 时 HTTP 仍 200。
|
||
def test_mixed_batch_still_http_200(client):
|
||
test_client, pool = client
|
||
_post(test_client, {"data": [_item(omsId=1)]})
|
||
resp = _post(
|
||
test_client,
|
||
{
|
||
"data": [
|
||
_item(omsId=1),
|
||
_item(omsId=2),
|
||
_item(omsId=3, orgCode=""),
|
||
]
|
||
},
|
||
)
|
||
assert resp.status_code == 200
|
||
codes = [r["resultCode"] for r in resp.json()["results"]]
|
||
assert codes == ["DUPLICATE", "ACCEPTED", "REJECTED"]
|
||
assert set(pool.db.sales) == {1, 2}
|
||
|
||
|
||
# JSON 字符串 omsId 不转整数,标 REJECTED。
|
||
def test_string_oms_id_is_rejected(client):
|
||
test_client, pool = client
|
||
resp = _post(test_client, {"data": [_item(omsId="2026092801")]})
|
||
assert resp.status_code == 200
|
||
assert resp.json()["results"][0]["resultCode"] == "REJECTED"
|
||
assert pool.db.sales == {}
|
||
|
||
|
||
# 库故障时 HTTP 503,body 固定「数据库不可用」,不含 ORA 和表名。
|
||
def test_repo_unavailable_returns_503_without_ora(client):
|
||
test_client, pool = client
|
||
pool.db.raise_execute = ora_error(
|
||
12170,
|
||
"ORA-12170: TNS:Connect timeout occurred at rpa_sales_goods_mapping_data",
|
||
)
|
||
resp = _post(test_client, {"data": [_item()]})
|
||
assert resp.status_code == 503
|
||
body = resp.json()
|
||
assert body["code"] == 503
|
||
assert body["msg"] == "数据库不可用"
|
||
assert body["results"] == []
|
||
text = resp.text
|
||
assert "ORA-" not in text
|
||
assert "rpa_sales_goods_mapping_data" not in text
|
||
|
||
|
||
# 一批两条合法明细只从池里借一次连接。
|
||
def test_batch_two_rows_acquires_once(client):
|
||
test_client, pool = client
|
||
resp = _post(
|
||
test_client,
|
||
{"data": [_item(omsId=1), _item(omsId=2)]},
|
||
)
|
||
assert resp.status_code == 200
|
||
assert [r["resultCode"] for r in resp.json()["results"]] == ["ACCEPTED", "ACCEPTED"]
|
||
assert pool.acquires == 1
|
||
many = [c for c in pool.db.calls if c[0] == "executemany"]
|
||
assert len(many) == 1
|
||
|
||
|
||
# 并发撞上未知唯一约束时不回 DUPLICATE,整包 503。
|
||
def test_unknown_unique_constraint_is_not_duplicate(client):
|
||
test_client, pool = client
|
||
pool.db.collide_sales.add(2026092801)
|
||
pool.db.collide_constraint = "UK_SOMETHING_ELSE"
|
||
resp = _post(test_client, {"data": [_item()]})
|
||
assert resp.status_code == 503
|
||
assert resp.json()["msg"] == "数据库不可用"
|
||
assert "DUPLICATE" not in resp.text
|
||
|
||
|
||
# 成功收件写审计一行,入参出参 JSON 里没有 token 明文。
|
||
def test_success_audit_masks_token(client):
|
||
test_client, pool = client
|
||
resp = _post(test_client, {"data": [_item()]})
|
||
assert resp.status_code == 200
|
||
assert len(pool.db.audits) == 1
|
||
audit = pool.db.audits[0]
|
||
blob = " ".join(str(v) for v in audit.values())
|
||
assert TOKEN not in blob
|
||
assert audit["token_masked"] == "***"
|
||
assert audit["api_no"] == 1
|
||
assert audit["http_code"] == 200
|
||
audit_sql = [c for c in pool.db.calls if c[0] == "execute" and "RPA_OMS_INBOUND_AUDIT" in " ".join(c[1].split()).upper()]
|
||
assert audit_sql
|