# 接口 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