""" MQTTSubscribeClient 单元测试 覆盖:主题通配符编译、路由构建、消息分发、请求-响应(reply_topic)与不回复约定。 """ import json from chongming_mqtt.mqtt_subscribe import MQTTSubscribeClient, _compile_topic from fakes import attach, run # ---------- _compile_topic 通配符匹配 ---------- def test_compile_topic_exact(): p = _compile_topic("/edge_app_controller") assert p.match("/edge_app_controller") assert not p.match("/edge_app_controller/other") assert not p.match("/other") def test_compile_topic_plus_wildcard(): """+ 只匹配单级""" p = _compile_topic("/dev/+/cmd") assert p.match("/dev/box1/cmd") assert not p.match("/dev/box1/sub/cmd") def test_compile_topic_hash_wildcard(): """# 匹配多级且须在末尾""" p = _compile_topic("/dev/#") assert p.match("/dev/a/b/c") assert p.match("/dev/x") assert not p.match("/other/a") # ---------- 路由构建 ---------- def test_routes_skip_invalid(): """无效(空 topic / 无 handler)订阅项应被跳过""" client = MQTTSubscribeClient("mqtt://h:1883", routes=[ {"topic": "", "handler": lambda d, t: None}, {"topic": "/t", "handler": lambda d, t: None}, {"topic": "/t2"}, # 无 handler ]) assert len(client._routes) == 1 # ---------- 消息分发与回复 ---------- async def _echo_handler(payload, topic): return {"echo": payload.get("v")} def test_dispatch_invokes_handler_and_replies(): """收到消息 → 调用 handler → 结果发布到 reply_topic""" client = MQTTSubscribeClient("mqtt://h:1883", routes=[ {"topic": "/req", "handler": _echo_handler, "reply_topic": "/resp"}, ]) fake = attach(client) run(client._dispatch("/req", b'{"v": 7}')) assert len(fake.published) == 1 topic, payload, _ = fake.published[0] assert topic == "/resp" assert json.loads(payload) == {"echo": 7} def test_dispatch_no_reply_when_handler_returns_none(): """handler 返回 None 时不发布回复(静默命令)""" async def silent(payload, topic): return None client = MQTTSubscribeClient("mqtt://h:1883", routes=[ {"topic": "/req", "handler": silent, "reply_topic": "/resp"}, ]) fake = attach(client) run(client._dispatch("/req", b'{"v": 1}')) assert fake.published == [] def test_dispatch_no_reply_without_reply_topic(): """未配置 reply_topic 时不发布""" client = MQTTSubscribeClient("mqtt://h:1883", routes=[ {"topic": "/req", "handler": _echo_handler}, ]) fake = attach(client) run(client._dispatch("/req", b'{"v": 1}')) assert fake.published == [] def test_dispatch_ignores_non_json(): """非 JSON 消息被忽略,不调用 handler""" calls = [] async def h(payload, topic): calls.append(payload) client = MQTTSubscribeClient("mqtt://h:1883", routes=[ {"topic": "/req", "handler": h, "reply_topic": "/resp"}, ]) fake = attach(client) run(client._dispatch("/req", b"not-json")) assert calls == [] assert fake.published == [] def test_dispatch_unmatched_topic_ignored(): """不匹配任何路由主题的消息被忽略""" client = MQTTSubscribeClient("mqtt://h:1883", routes=[ {"topic": "/req", "handler": _echo_handler, "reply_topic": "/resp"}, ]) fake = attach(client) run(client._dispatch("/other/topic", b'{"v": 1}')) assert fake.published == [] def test_reply_topic_placeholder_fill(): """reply_topic 的 {1} 占位符填充订阅主题通配符捕获值""" client = MQTTSubscribeClient("mqtt://h:1883", routes=[ {"topic": "/dev/+/cmd", "handler": _echo_handler, "reply_topic": "/resp/{1}"}, ]) fake = attach(client) run(client._dispatch("/dev/box9/cmd", b'{"v": 1}')) assert fake.published[0][0] == "/resp/box9"