Files
inventory_analytics/tests/test_channel_inventory_schema.py
T
BXY0414 e234bfbe7a 修feat: 渠道库存同步支持字段任意排序并增加字段异常告警
- 按字段名校验并写入渠道库存,取消字段顺序校验及表头重建
- 严格校验必填字段名称、类型和数字格式,失败时不修改表头或记录
- 字段校验失败时发送飞书群通知
- 添加字段校验与通知流程测试
- 忽略 Python 字节码、__pycache__ 和 .DS_Store
2026-07-22 14:28:23 +08:00

101 lines
4.3 KiB
Python

from __future__ import annotations
import json
import unittest
from unittest.mock import Mock
from inventory_sync.feishu import FeishuClient, FeishuFieldValidationError
from inventory_sync.service import CHANNEL_INVENTORY_SCHEMA_ALERT_CHAT_ID, sync_brand_target
from inventory_sync.specs import BRAND_SYNC_TARGETS, FIELD_SPECS, PRIMARY_FIELD_NAME
def valid_fields() -> list[dict[str, object]]:
fields: list[dict[str, object]] = [
{"field_name": PRIMARY_FIELD_NAME, "is_primary": True, "type": 1},
]
for spec in FIELD_SPECS:
field: dict[str, object] = {
"field_name": spec.feishu_name,
"is_primary": False,
"type": spec.field_type,
}
if spec.formatter is not None:
field["property"] = {"formatter": spec.formatter}
fields.append(field)
return fields
class ChannelInventorySchemaTests(unittest.TestCase):
def setUp(self) -> None:
self.client = FeishuClient(None) # type: ignore[arg-type]
self.client.list_fields = Mock() # type: ignore[method-assign]
def test_allows_fields_in_any_order(self) -> None:
fields = valid_fields()
self.client.list_fields.return_value = [fields[0], *reversed(fields[1:])]
available_fields = self.client.ensure_target_fields("token", "app", "table")
self.assertEqual(available_fields, [field["field_name"] for field in [fields[0], *reversed(fields[1:])]])
def test_rejects_missing_or_renamed_required_field(self) -> None:
fields = valid_fields()
fields = [field for field in fields if field["field_name"] != "品牌"]
self.client.list_fields.return_value = fields
with self.assertRaisesRegex(FeishuFieldValidationError, "缺少必需字段或字段已改名:“品牌”"):
self.client.ensure_target_fields("token", "app", "table")
def test_rejects_wrong_type_and_formatter(self) -> None:
fields = valid_fields()
price_field = next(field for field in fields if field["field_name"] == "零售价")
price_field["type"] = 1
price_field["property"] = {"formatter": "0"}
self.client.list_fields.return_value = fields
with self.assertRaisesRegex(FeishuFieldValidationError, "字段类型错误:“零售价”") as context:
self.client.ensure_target_fields("token", "app", "table")
self.assertIn("字段格式错误:“零售价”", str(context.exception))
def test_sends_group_text_message(self) -> None:
request_json = Mock(return_value={"code": 0})
self.client.request_json = request_json # type: ignore[method-assign]
self.client.send_text_message("token", "oc_example", "字段校验失败")
url = request_json.call_args.args[0]
payload = request_json.call_args.kwargs["payload"]
self.assertEqual(url, "https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id")
self.assertEqual(payload["receive_id"], "oc_example")
self.assertEqual(json.loads(payload["content"]), {"text": "字段校验失败"})
def test_schema_failure_notifies_group_before_records_are_changed(self) -> None:
class SchemaFailureClient:
def __init__(self) -> None:
self.notifications: list[tuple[str, str, str]] = []
def ensure_target_fields(self, *_args: object) -> list[str]:
raise FeishuFieldValidationError("缺少必需字段或字段已改名:“品牌”")
def send_text_message(self, access_token: str, chat_id: str, text: str) -> None:
self.notifications.append((access_token, chat_id, text))
client = SchemaFailureClient()
target = BRAND_SYNC_TARGETS[0]
with self.assertRaises(FeishuFieldValidationError):
sync_brand_target(
client, # type: ignore[arg-type]
"token",
"app",
[{"name": target.table_name, "table_id": "table"}],
target,
[],
)
self.assertEqual(len(client.notifications), 1)
_, chat_id, text = client.notifications[0]
self.assertEqual(chat_id, CHANNEL_INVENTORY_SCHEMA_ALERT_CHAT_ID)
self.assertIn("不会删除或重建字段,也不会删除或写入记录", text)