mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-08 09:52:55 +02:00
fix(companion): protect direct offline messages
This commit is contained in:
@@ -5,7 +5,7 @@ from typing import Any, cast
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from openhop_core.companion.constants import RESP_CODE_NO_MORE_MESSAGES
|
||||
from openhop_core.companion.constants import PUSH_CODE_MSG_WAITING, RESP_CODE_NO_MORE_MESSAGES
|
||||
|
||||
from repeater.companion.bridge import RepeaterCompanionBridge, _to_json_safe
|
||||
from repeater.companion.frame_server import CompanionFrameServer
|
||||
@@ -201,6 +201,24 @@ async def test_frame_server_no_more_messages_response_when_empty():
|
||||
assert srv._write_frame.call_args[0][0] == bytes([RESP_CODE_NO_MORE_MESSAGES])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rejected_queue_callback_skips_sqlite_persistence_but_notifies_client():
|
||||
server = object.__new__(CompanionFrameServer)
|
||||
server._persist_companion_message = AsyncMock()
|
||||
server._enqueue_frame = MagicMock()
|
||||
|
||||
await server._on_message_received(
|
||||
b"\x01" * 32,
|
||||
"rejected",
|
||||
1,
|
||||
0,
|
||||
queued=False,
|
||||
)
|
||||
|
||||
server._persist_companion_message.assert_not_awaited()
|
||||
server._enqueue_frame.assert_called_once_with(bytes([PUSH_CODE_MSG_WAITING]))
|
||||
|
||||
|
||||
def test_companion_utils_validation_and_normalization():
|
||||
assert normalize_companion_identity_key(" 0xAABB ") == "AABB"
|
||||
assert validate_companion_node_name(" node-1 ") == "node-1"
|
||||
|
||||
@@ -176,9 +176,54 @@ class TestSqliteRetentionTrim:
|
||||
|
||||
def test_trims_to_max_messages(self, tmp_path):
|
||||
h = self._handler(tmp_path)
|
||||
for i in range(5):
|
||||
self._push(h, "0x01", i, max_messages=3)
|
||||
assert len(h.companion_load_messages("0x01")) == 3
|
||||
results = [self._push(h, "0x01", i, max_messages=3) for i in range(5)]
|
||||
assert results == [True, True, True, False, False]
|
||||
assert [m["text"] for m in h.companion_load_messages("0x01")] == ["m0", "m1", "m2"]
|
||||
|
||||
def test_evicts_oldest_channel_message_before_direct_message(self, tmp_path):
|
||||
h = self._handler(tmp_path)
|
||||
direct_one = {"text": "direct one", "packet_hash": "d1", "is_channel": False}
|
||||
channel_one = {"text": "channel one", "packet_hash": "c1", "is_channel": True}
|
||||
direct_two = {"text": "direct two", "packet_hash": "d2", "is_channel": False}
|
||||
|
||||
assert h.companion_push_message("0x01", direct_one, max_messages=2)
|
||||
assert h.companion_push_message("0x01", channel_one, max_messages=2)
|
||||
assert h.companion_push_message("0x01", direct_two, max_messages=2)
|
||||
|
||||
messages = h.companion_load_messages("0x01")
|
||||
assert [m["text"] for m in messages] == ["direct one", "direct two"]
|
||||
assert [m["is_channel"] for m in messages] == [0, 0]
|
||||
|
||||
def test_rejects_channel_when_queue_contains_only_direct_messages(self, tmp_path):
|
||||
h = self._handler(tmp_path)
|
||||
for packet_hash in ("d1", "d2"):
|
||||
assert h.companion_push_message(
|
||||
"0x01", {"text": packet_hash, "packet_hash": packet_hash}, max_messages=2
|
||||
)
|
||||
|
||||
assert not h.companion_push_message(
|
||||
"0x01", {"text": "channel", "packet_hash": "c1", "is_channel": True}, max_messages=2
|
||||
)
|
||||
assert [m["text"] for m in h.companion_load_messages("0x01")] == ["d1", "d2"]
|
||||
|
||||
def test_rejected_insert_keeps_existing_channels_when_limit_is_lowered(self, tmp_path):
|
||||
h = self._handler(tmp_path)
|
||||
existing = [
|
||||
{"text": "direct one", "packet_hash": "d1", "is_channel": False},
|
||||
{"text": "channel one", "packet_hash": "c1", "is_channel": True},
|
||||
{"text": "direct two", "packet_hash": "d2", "is_channel": False},
|
||||
]
|
||||
for message in existing:
|
||||
assert h.companion_push_message("0x01", message)
|
||||
|
||||
assert not h.companion_push_message(
|
||||
"0x01", {"text": "incoming", "packet_hash": "d3"}, max_messages=2
|
||||
)
|
||||
assert [m["text"] for m in h.companion_load_messages("0x01")] == [
|
||||
"direct one",
|
||||
"channel one",
|
||||
"direct two",
|
||||
]
|
||||
|
||||
def test_none_keeps_all(self, tmp_path):
|
||||
h = self._handler(tmp_path)
|
||||
@@ -381,6 +426,14 @@ class TestPersistSkipWhenOff:
|
||||
asyncio.run(fs._persist_companion_message({"text": "x"}))
|
||||
fs.sqlite_handler.companion_push_message.assert_called_once_with("0x01", {"text": "x"}, 7)
|
||||
|
||||
def test_keeps_memory_message_when_sqlite_rejects_it(self):
|
||||
import asyncio
|
||||
|
||||
fs = self._frame_server(7)
|
||||
fs.sqlite_handler.companion_push_message.return_value = False
|
||||
asyncio.run(fs._persist_companion_message({"text": "x"}))
|
||||
fs.bridge.message_queue.pop_last.assert_not_called()
|
||||
|
||||
|
||||
class TestImportRepeaterContactsCap:
|
||||
"""The import endpoint must never leave persisted contacts above max_contacts.
|
||||
|
||||
Reference in New Issue
Block a user