251 lines
8.2 KiB
Python
251 lines
8.2 KiB
Python
# 接口 3 收件契约。假连接录 SQL,不连 Oracle。按对方 fid 幂等。
|
||
|
||
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/purchase-contracts"
|
||
|
||
|
||
# 构造一条合法的接口 3 明细,overrides 覆盖字段。
|
||
def _item(**overrides):
|
||
row = {
|
||
"fid": 200001,
|
||
"omsId": 20260928000001,
|
||
"supplierId": "SUP001",
|
||
"supplierName": "示例供应商",
|
||
"contractSignDate": "2026-09-28",
|
||
"contractStartDate": "2026-10-01",
|
||
"contractEndDate": "2027-09-30",
|
||
"paymentTerm": 90,
|
||
"goodsId": "G000123456789",
|
||
"goodsName": "阿莫西林胶囊",
|
||
"regNo": "国药准字H20201234",
|
||
"spec": "0.25g*24粒",
|
||
"unit": "盒",
|
||
"qty": "10.0000",
|
||
"purchaseTaxRate": "0.13",
|
||
"unitPrice": "100.0000",
|
||
"goodsSourceId": "GS001",
|
||
"purchaserId": "P001",
|
||
"purchaserName": "张三",
|
||
"plannerId": "PL001",
|
||
"plannerName": "李四",
|
||
"contractEntryStatus": None,
|
||
"contractNo": None,
|
||
"orgCode": "901",
|
||
"customerType": 1,
|
||
"goodsMdmCode": "QX0100167583",
|
||
"supplierMdmCode": "DW0100046361",
|
||
}
|
||
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
|
||
assert resp.json()["results"] == []
|
||
assert pool.db.audits
|
||
|
||
|
||
# 缺 data 节点时整包 422,一条都不落。
|
||
def test_missing_data_returns_422(client):
|
||
test_client, pool = client
|
||
resp = _post(test_client, {})
|
||
assert resp.status_code == 422
|
||
assert pool.db.contracts == {}
|
||
|
||
|
||
# 超过 100 条时整包 422,假库仍空。
|
||
def test_over_100_rows_returns_422(client):
|
||
test_client, pool = client
|
||
rows = [_item(fid=i, omsId=i) for i in range(1, 102)]
|
||
resp = _post(test_client, {"data": rows})
|
||
assert resp.status_code == 422
|
||
assert pool.db.contracts == {}
|
||
|
||
|
||
# 合法一行 ACCEPTED,process_status=0,fid 原样落库。
|
||
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"] == [
|
||
{
|
||
"fid": 200001,
|
||
"resultCode": "ACCEPTED",
|
||
"message": "采购合同数据已接收,等待处理",
|
||
}
|
||
]
|
||
stored = pool.db.contracts[200001]
|
||
assert stored["process_status"] == 0
|
||
assert stored["oms_id"] == 20260928000001
|
||
assert stored["supplier_id"] == "SUP001"
|
||
assert stored["goods_mdm_code"] == "QX0100167583"
|
||
assert stored["contract_entry_status"] is None
|
||
assert stored["contract_no"] is None
|
||
|
||
|
||
# 跨请求同 fid:DUPLICATE,库里品名不改。
|
||
def test_existing_fid_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 result["message"] == "序号已存在,未重复入库"
|
||
assert pool.db.contracts[200001]["goods_name"] == "阿莫西林胶囊"
|
||
|
||
|
||
# 同一 POST 里两条相同 fid:后一条 CONFLICT,只落第一条。
|
||
def test_duplicate_fid_in_same_batch_is_conflict(client):
|
||
test_client, pool = client
|
||
resp = _post(
|
||
test_client,
|
||
{"data": [_item(), _item(omsId=20260928000002)]},
|
||
)
|
||
assert resp.status_code == 200
|
||
results = resp.json()["results"]
|
||
assert results[0]["resultCode"] == "ACCEPTED"
|
||
assert results[1]["resultCode"] == "CONFLICT"
|
||
assert results[1]["fid"] == 200001
|
||
assert len(pool.db.contracts) == 1
|
||
|
||
|
||
# contractEntryStatus=0 却没有合同号时 REJECTED。
|
||
def test_retry_protocol_without_contract_no_is_rejected(client):
|
||
test_client, pool = client
|
||
resp = _post(test_client, {"data": [_item(contractEntryStatus=0, contractNo=None)]})
|
||
assert resp.status_code == 200
|
||
result = resp.json()["results"][0]
|
||
assert result["resultCode"] == "REJECTED"
|
||
assert "contractNo" in result["message"]
|
||
assert pool.db.contracts == {}
|
||
|
||
|
||
# contractEntryStatus=0 且带合同号时 ACCEPTED。
|
||
def test_retry_protocol_with_contract_no_is_accepted(client):
|
||
test_client, pool = client
|
||
resp = _post(
|
||
test_client,
|
||
{"data": [_item(fid=200002, contractEntryStatus=0, contractNo="HT20260928002")]},
|
||
)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["results"][0]["resultCode"] == "ACCEPTED"
|
||
stored = pool.db.contracts[200002]
|
||
assert stored["contract_entry_status"] == 0
|
||
assert stored["contract_no"] == "HT20260928002"
|
||
|
||
|
||
# 结束日早于开始日时 REJECTED。
|
||
def test_end_date_before_start_is_rejected(client):
|
||
test_client, pool = client
|
||
resp = _post(
|
||
test_client,
|
||
{"data": [_item(contractStartDate="2027-09-30", contractEndDate="2026-10-01")]},
|
||
)
|
||
assert resp.status_code == 200
|
||
assert resp.json()["results"][0]["resultCode"] == "REJECTED"
|
||
assert pool.db.contracts == {}
|
||
|
||
|
||
# JSON 字符串 omsId 不转整数,标 REJECTED。
|
||
def test_string_oms_id_is_rejected(client):
|
||
test_client, pool = client
|
||
resp = _post(test_client, {"data": [_item(omsId="20260928000001")]})
|
||
assert resp.status_code == 200
|
||
assert resp.json()["results"][0]["resultCode"] == "REJECTED"
|
||
assert pool.db.contracts == {}
|
||
|
||
|
||
# 缺货品 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.contracts == {}
|
||
|
||
|
||
# 混批 DUPLICATE + ACCEPTED + REJECTED 时 HTTP 仍 200。
|
||
def test_mixed_batch_still_http_200(client):
|
||
test_client, pool = client
|
||
_post(test_client, {"data": [_item(fid=1, omsId=1)]})
|
||
resp = _post(
|
||
test_client,
|
||
{
|
||
"data": [
|
||
_item(fid=1, omsId=1),
|
||
_item(fid=2, omsId=2),
|
||
_item(fid=3, 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.contracts) == {1, 2}
|
||
|
||
|
||
# 库故障时 HTTP 503,body 不含 ORA。
|
||
def test_repo_unavailable_returns_503(client):
|
||
test_client, pool = client
|
||
pool.db.raise_execute = ora_error(
|
||
12170,
|
||
"ORA-12170: TNS:Connect timeout occurred at rpa_purchase_contract_data",
|
||
)
|
||
resp = _post(test_client, {"data": [_item()]})
|
||
assert resp.status_code == 503
|
||
assert resp.json()["code"] == 503
|
||
assert resp.json()["msg"] == "数据库不可用"
|
||
assert resp.json()["results"] == []
|
||
assert "ORA-" not in resp.text
|
||
assert "rpa_purchase_contract_data" not in resp.text
|
||
|
||
|
||
# 一批两条合法明细只从池里借一次连接。
|
||
def test_batch_two_rows_acquires_once(client):
|
||
test_client, pool = client
|
||
resp = _post(
|
||
test_client,
|
||
{"data": [_item(fid=1, omsId=1), _item(fid=2, 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
|