#!/usr/bin/env python3 """消息模块纯标准库层的测试:主题方案 / 消息封皮与日志 / 统一总线。 不 import Qt、不连 broker——这一层必须能在最小环境里跑通(分层约定见 src/svfs_client/__init__.py)。 """ import sys import unittest from helpers import VFSTestCase from svfs_client import bus as bus_mod from svfs_client import messages as messages_mod from svfs_client import topics assert sys.modules.get("helpers") is not None class TopicTest(unittest.TestCase): """主题方案:组织位、转义可逆、通配订阅。""" ORG = "acme" USER = "alice" def test_topics_carry_org_and_user(self): self.assertEqual( topics.user_message_topic("svfs", self.ORG, self.USER), "svfs/acme/user/alice/message") self.assertEqual( topics.user_plugin_topic("svfs", self.ORG, self.USER, "com.example.clock"), "svfs/acme/user/alice/plugin/com.example.clock") self.assertEqual( topics.broadcast_message_topic("svfs", self.ORG), "svfs/acme/broadcast/message") self.assertEqual( topics.broadcast_plugin_topic("svfs", self.ORG, "com.example.clock"), "svfs/acme/broadcast/plugin/com.example.clock") def test_defaults_fill_in_prefix_and_org(self): self.assertEqual(topics.user_message_topic("", "", "alice"), "svfs/default/user/alice/message") # 前后多余的斜杠不该造出空层级 self.assertEqual(topics.broadcast_message_topic("/svfs/", "acme"), "svfs/acme/broadcast/message") def test_parse_roundtrip(self): topic = topics.user_plugin_topic("svfs", self.ORG, self.USER, "com.example.x") info = topics.parse(topic, "svfs") self.assertEqual(info["org"], self.ORG) self.assertEqual(info["scope"], "user") self.assertEqual(info["user"], self.USER) self.assertEqual(info["kind"], "plugin") self.assertEqual(info["plugin"], "com.example.x") info = topics.parse(topics.broadcast_message_topic("svfs", self.ORG), "svfs") self.assertEqual((info["scope"], info["kind"], info["user"], info["plugin"]), ("broadcast", "message", "", "")) def test_parse_rejects_other_schemes(self): for topic in ("", "svfs", "svfs/acme", "svfs/acme/user", "other/acme/user/alice/message", # 前缀不对 "svfs/acme/user/alice/unknown", # 类别不认识 "svfs/acme/user/alice/plugin", # 少了插件 id "svfs/acme/broadcast", # 少了类别 "svfs/acme/broadcast/plugin/"): # 空插件 id self.assertIsNone(topics.parse(topic, "svfs"), topic) def test_unsafe_segments_are_escaped_and_invertible(self): # 空格、中文、斜杠都不能破坏层级;解析回来必须还是原值 for user in ("a b", "张三", "a/b", "a+b", "a#b", "a@corp.example"): topic = topics.user_message_topic("svfs", self.ORG, user) self.assertEqual(len(topic.split("/")), 5, topic) self.assertEqual(topics.parse(topic, "svfs")["user"], user) def test_escaping_never_collides(self): # 转义必须是可逆的:不能把两个不同用户映射到同一个主题 self.assertNotEqual(topics.topic_segment("a b"), topics.topic_segment("a_b")) self.assertNotEqual(topics.topic_segment("a/b"), topics.topic_segment("ab")) self.assertEqual(topics.topic_segment("safe.name-1@x"), "safe.name-1@x") def test_empty_segment_is_rejected(self): with self.assertRaises(ValueError): topics.topic_segment("") with self.assertRaises(ValueError): topics.user_message_topic("svfs", self.ORG, "") def test_subscriptions_are_least_privilege(self): subs = topics.subscriptions("svfs", self.ORG, self.USER) self.assertEqual(subs, ["svfs/acme/user/alice/#", "svfs/acme/broadcast/#"]) # 不订阅整个组织的 #(多租户下这正是 ACL 要挡的范围) self.assertNotIn("svfs/acme/#", subs) class MessageLogTest(VFSTestCase): """消息封皮解析与消息中心列表。""" def test_parse_json_envelope(self): message = messages_mod.parse_message( '{"id": "m1", "title": "备份完成", "body": "3 个包",' ' "level": "warn", "time": "2026-09-16T10:00:00", "source": "backup"}') self.assertEqual(message["id"], "m1") self.assertEqual(message["title"], "备份完成") self.assertEqual(message["body"], "3 个包") self.assertEqual(message["level"], "warn") self.assertEqual(message["time"], "2026-09-16T10:00:00") self.assertEqual(message["source"], "backup") self.assertTrue(message["unread"]) def test_parse_plain_and_scalar_payloads(self): self.assertEqual(messages_mod.parse_message(" 开机完成 ")["body"], "开机完成") self.assertEqual(messages_mod.parse_message('"一句话"')["body"], "一句话") self.assertEqual(messages_mod.parse_message("[1,2]")["body"], "[1, 2]") self.assertEqual(messages_mod.parse_message(b"\xe4\xb8\xad\xe6\x96\x87")["body"], "中文") def test_parse_rejects_empty_and_contentless(self): for payload in (None, "", " ", "{}", '{"title": ""}', b""): self.assertIsNone(messages_mod.parse_message(payload), repr(payload)) def test_parse_normalizes_level_and_time(self): message = messages_mod.parse_message('{"body": "x", "level": "FATAL"}', received_time="2026-09-16T12:00:00") self.assertEqual(message["level"], "info") # 新级别也当 info,消息不能丢 self.assertEqual(message["time"], "2026-09-16T12:00:00") # 缺失时用收到时间 self.assertTrue(messages_mod.parse_message('{"body": "x"}')["time"]) def test_parse_caps_length(self): message = messages_mod.parse_message( '{"title": "%s", "body": "%s"}' % ("t" * 500, "b" * 9000)) self.assertLessEqual(len(message["title"]), 200) self.assertLessEqual(len(message["body"]), messages_mod.MAX_TEXT) def test_log_dedupes_by_id(self): log = messages_mod.MessageLog() self.assertIsNotNone(log.add(messages_mod.parse_message('{"id": "m1", "body": "a"}'))) # 同 id 重投(QoS 1 重发 / 补齐与推送重叠)→ 丢掉 self.assertIsNone(log.add(messages_mod.parse_message('{"id": "m1", "body": "a"}'))) self.assertEqual(len(log), 1) def test_log_dedupes_id_less_messages_by_content_and_second(self): """服务端没给 id 时按"时间(秒)+ 标题 + 正文"去重——这正是没有 id 的 消息同时经推送与 HTTP 补齐两条路到达的场景,显示一条才对。""" log = messages_mod.MessageLog() payload = '{"body": "同一条", "time": "2026-09-16T10:00:00"}' self.assertIsNotNone(log.add(messages_mod.parse_message(payload))) self.assertIsNone(log.add(messages_mod.parse_message(payload))) self.assertEqual(len(log), 1) # 时间不同(真·第二条)或内容不同都算新消息 self.assertIsNotNone(log.add(messages_mod.parse_message( '{"body": "同一条", "time": "2026-09-16T10:00:01"}'))) self.assertIsNotNone(log.add(messages_mod.parse_message( '{"body": "另一条", "time": "2026-09-16T10:00:01"}'))) self.assertEqual(len(log), 3) def test_log_unread_and_mark_read(self): log = messages_mod.MessageLog() for index in range(3): log.add(messages_mod.parse_message(f'{{"id": "m{index}", "body": "x"}}')) self.assertEqual(log.unread(), 3) self.assertEqual(log.mark_all_read(), 3) self.assertEqual(log.unread(), 0) self.assertEqual(log.mark_all_read(), 0) log.add(messages_mod.parse_message('{"id": "m9", "body": "y"}')) self.assertTrue(log.mark_read("m9")) self.assertFalse(log.mark_read("m9")) # 已经读过 self.assertFalse(log.mark_read("nope")) def test_log_caps_and_forgets_dropped_keys(self): log = messages_mod.MessageLog(limit=3) for index in range(5): log.add(messages_mod.parse_message(f'{{"id": "m{index}", "body": "x"}}')) self.assertEqual([m["id"] for m in log.items()], ["m2", "m3", "m4"]) # 被挤掉的 id 不再占去重表:同样的 id 再来是"新消息"(而不是永久沉默) self.assertIsNotNone( log.add(messages_mod.parse_message('{"id": "m0", "body": "x"}'))) def test_log_clear(self): log = messages_mod.MessageLog() log.add(messages_mod.parse_message('{"id": "m1", "body": "x"}')) log.clear() self.assertEqual(len(log), 0) self.assertEqual(log.unread(), 0) self.assertIsNotNone( log.add(messages_mod.parse_message('{"id": "m1", "body": "x"}'))) class MessageBusTest(VFSTestCase): """总线的分类、归属校验与分发。""" ORG = "acme" USER = "alice" def setUp(self): super().setUp() self.bus = bus_mod.MessageBus(org=self.ORG, user=self.USER) def _deliver(self, topic, payload, received_time=""): return self.bus.deliver(topic, payload, received_time=received_time) # ---- 分类 ------------------------------------------------------- def test_host_message_goes_to_message_log(self): topic = topics.user_message_topic("svfs", self.ORG, self.USER) result = self._deliver(topic, '{"id": "m1", "title": "备份完成"}') self.assertEqual(result["action"], "host") self.assertEqual(len(self.bus.log), 1) self.assertEqual(self.bus.log.items()[0]["title"], "备份完成") self.assertEqual(self.bus.log.items()[0]["scope"], "user") def test_plugin_message_never_reaches_message_log(self): topic = topics.user_plugin_topic("svfs", self.ORG, self.USER, "com.example.dash") result = self._deliver(topic, '{"cpu": 0.42}') self.assertEqual(result["action"], "plugin") self.assertEqual(result["plugin"], "com.example.dash") self.assertEqual(len(self.bus.log), 0, "插件消息不能进消息中心") # 插件取走原文(宿主不解析插件协议) drained = self.bus.drain("com.example.dash") self.assertEqual(len(drained), 1) self.assertEqual(drained[0]["payload"], '{"cpu": 0.42}') self.assertEqual(self.bus.drain("com.example.dash"), []) def test_broadcast_plugin_message(self): topic = topics.broadcast_plugin_topic("svfs", self.ORG, "com.example.dash") result = self._deliver(topic, "tick") self.assertEqual(result["action"], "plugin") self.assertEqual(result["scope"], "broadcast") self.assertEqual(self.bus.pending("com.example.dash"), 1) def test_broadcast_host_message_is_marked(self): topic = topics.broadcast_message_topic("svfs", self.ORG) self._deliver(topic, '{"body": "全员通知"}') self.assertEqual(self.bus.log.items()[0]["scope"], "broadcast") def test_unknown_topic_and_empty_payload(self): self.assertEqual(self._deliver("svfs/acme/user/alice/nope", "x")["action"], "unknown") topic = topics.user_message_topic("svfs", self.ORG, self.USER) self.assertEqual(self._deliver(topic, " ")["action"], "empty") self.assertEqual(len(self.bus.log), 0) # ---- 归属校验 --------------------------------------------------- def test_other_org_or_user_is_rejected(self): other_org = topics.user_message_topic("svfs", "other", self.USER) self.assertEqual(self._deliver(other_org, '{"body": "x"}')["action"], "forbidden") other_user = topics.user_message_topic("svfs", self.ORG, "bob") self.assertEqual(self._deliver(other_user, '{"body": "x"}')["action"], "forbidden") forbidden_plugin = topics.user_plugin_topic("svfs", self.ORG, "bob", "com.example.dash") self.assertEqual(self._deliver(forbidden_plugin, "x")["action"], "forbidden") self.assertEqual(len(self.bus.log), 0) self.assertEqual(self.bus.pending("com.example.dash"), 0) self.assertEqual(self.bus.stats()["forbidden"], 3) def test_identify_switches_tenant_and_clears_buffers(self): self._deliver(topics.user_plugin_topic("svfs", self.ORG, self.USER, "com.a"), "x") self.assertEqual(self.bus.pending("com.a"), 1) self.bus.identify(org="other", user="bob") self.assertEqual(self.bus.pending("com.a"), 0, "换身份后不该留着旧队列") self.assertEqual( self._deliver(topics.user_message_topic("svfs", self.ORG, self.USER), '{"body": "x"}')["action"], "forbidden") def test_bus_rejects_everything_before_identify(self): """没有身份(未登录/已退出登录)时一条都不收。 消息模块只在登录后工作;退出登录后残留的推送不该落进消息中心。 """ bus = bus_mod.MessageBus() result = bus.deliver(topics.broadcast_message_topic("svfs", "acme"), '{"body": "x"}') self.assertEqual(result["action"], "forbidden") self.assertEqual(len(bus.log), 0) bus.identify(org="acme") self.assertEqual( bus.deliver(topics.broadcast_message_topic("svfs", "acme"), '{"body": "x"}')["action"], "host") # ---- 去重与兜底 ------------------------------------------------- def test_plugin_duplicates_are_dropped(self): topic = topics.user_plugin_topic("svfs", self.ORG, self.USER, "com.example.dash") self.assertEqual(self._deliver(topic, "same")["action"], "plugin") self.assertEqual(self._deliver(topic, "same")["action"], "duplicate") self.assertEqual(self.bus.pending("com.example.dash"), 1) # 内容不同就是新消息 self.assertEqual(self._deliver(topic, "other")["action"], "plugin") def test_plugin_buffer_drops_oldest_and_counts(self): bus = bus_mod.MessageBus(org=self.ORG, user=self.USER, plugin_buffer=3) topic = topics.user_plugin_topic("svfs", self.ORG, self.USER, "com.example.dash") for index in range(5): bus.deliver(topic, f"payload-{index}") drained = bus.drain("com.example.dash") self.assertEqual([item["payload"] for item in drained], ["payload-2", "payload-3", "payload-4"]) self.assertEqual(bus.stats()["dropped"], 2) def test_reset_clears_everything(self): self._deliver(topics.user_message_topic("svfs", self.ORG, self.USER), '{"id": "m1", "body": "x"}') self._deliver(topics.user_plugin_topic("svfs", self.ORG, self.USER, "com.a"), "x") self.bus.reset() self.assertEqual(len(self.bus.log), 0) self.assertEqual(self.bus.pending("com.a"), 0) self.assertEqual(self.bus.stats(), {}) def test_stats_track_each_channel(self): self._deliver(topics.user_message_topic("svfs", self.ORG, self.USER), '{"id": "m1", "body": "x"}') self._deliver(topics.user_message_topic("svfs", self.ORG, self.USER), '{"id": "m1", "body": "x"}') # 重投 self._deliver(topics.user_plugin_topic("svfs", self.ORG, self.USER, "com.a"), "x") stats = self.bus.stats() self.assertEqual(stats["received"], 3) self.assertEqual(stats["host"], 1) self.assertEqual(stats["plugin"], 1) self.assertEqual(stats["duplicate"], 1) self.assertEqual(self.bus.buffered_plugins(), ["com.a"]) if __name__ == "__main__": unittest.main()