Files
chongming-mqtt/test/test_subscribe.py
T

125 lines
3.8 KiB
Python

"""
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"