修feat: 渠道库存同步支持字段任意排序并增加字段异常告警
- 按字段名校验并写入渠道库存,取消字段顺序校验及表头重建 - 严格校验必填字段名称、类型和数字格式,失败时不修改表头或记录 - 字段校验失败时发送飞书群通知 - 添加字段校验与通知流程测试 - 忽略 Python 字节码、__pycache__ 和 .DS_Store
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user