mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-07-05 09:22:04 +02:00
Tighten up message broadcast contract
This commit is contained in:
@@ -203,6 +203,101 @@ class TestBotModuleParameterExtraction:
|
||||
assert captured["message_text"] == "the actual message"
|
||||
assert captured["sender_name"] == "Alice"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_channel_name_uses_payload_before_db_lookup(self):
|
||||
"""Channel fanout payload channel_name is preserved even if the DB lookup misses."""
|
||||
from app.fanout.bot import BotModule
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_execute(
|
||||
code,
|
||||
sender_name,
|
||||
sender_key,
|
||||
message_text,
|
||||
is_dm,
|
||||
channel_key,
|
||||
channel_name,
|
||||
sender_timestamp,
|
||||
path,
|
||||
is_outgoing,
|
||||
):
|
||||
captured["channel_name"] = channel_name
|
||||
return None
|
||||
|
||||
mod = BotModule("test", {"code": "def bot(**k): pass"}, name="Test")
|
||||
|
||||
with (
|
||||
patch("app.fanout.bot_exec.execute_bot_code", side_effect=fake_execute),
|
||||
patch(
|
||||
"app.fanout.bot_exec._bot_semaphore",
|
||||
MagicMock(__aenter__=AsyncMock(), __aexit__=AsyncMock()),
|
||||
),
|
||||
patch("app.fanout.bot.asyncio.sleep", new_callable=AsyncMock),
|
||||
patch("app.repository.ChannelRepository") as mock_chan,
|
||||
):
|
||||
mock_chan.get_by_key = AsyncMock(return_value=None)
|
||||
await mod._run_for_message(
|
||||
{
|
||||
"type": "CHAN",
|
||||
"conversation_key": "ch1",
|
||||
"channel_name": "#payload",
|
||||
"text": "Alice: hello",
|
||||
"sender_name": "Alice",
|
||||
}
|
||||
)
|
||||
|
||||
assert captured["channel_name"] == "#payload"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_dm_sender_name_uses_payload_before_db_lookup(self):
|
||||
"""Incoming DM sender_name from the message payload should be preserved."""
|
||||
from app.fanout.bot import BotModule
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_execute(
|
||||
code,
|
||||
sender_name,
|
||||
sender_key,
|
||||
message_text,
|
||||
is_dm,
|
||||
channel_key,
|
||||
channel_name,
|
||||
sender_timestamp,
|
||||
path,
|
||||
is_outgoing,
|
||||
):
|
||||
captured["sender_name"] = sender_name
|
||||
captured["sender_key"] = sender_key
|
||||
return None
|
||||
|
||||
mod = BotModule("test", {"code": "def bot(**k): pass"}, name="Test")
|
||||
|
||||
with (
|
||||
patch("app.fanout.bot_exec.execute_bot_code", side_effect=fake_execute),
|
||||
patch(
|
||||
"app.fanout.bot_exec._bot_semaphore",
|
||||
MagicMock(__aenter__=AsyncMock(), __aexit__=AsyncMock()),
|
||||
),
|
||||
patch("app.fanout.bot.asyncio.sleep", new_callable=AsyncMock),
|
||||
patch("app.repository.ContactRepository") as mock_contact,
|
||||
):
|
||||
mock_contact.get_by_key = AsyncMock(return_value=None)
|
||||
await mod._run_for_message(
|
||||
{
|
||||
"type": "PRIV",
|
||||
"conversation_key": "pk1",
|
||||
"sender_name": "PayloadAlice",
|
||||
"sender_key": "pk1",
|
||||
"text": "hello",
|
||||
"outgoing": False,
|
||||
}
|
||||
)
|
||||
|
||||
assert captured["sender_name"] == "PayloadAlice"
|
||||
assert captured["sender_key"] == "pk1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# T2: Migration 036, 037, 038 tests
|
||||
|
||||
@@ -196,6 +196,54 @@ class TestFanoutMqttIntegration:
|
||||
assert "alpha/dm:pk1" in topics
|
||||
assert "beta/dm:pk1" in topics
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_private_mqtt_preserves_full_message_payload(self, mqtt_broker, integration_db):
|
||||
"""Private MQTT publishes the full message payload without dropping fields."""
|
||||
from unittest.mock import patch
|
||||
|
||||
cfg = await FanoutConfigRepository.create(
|
||||
config_type="mqtt_private",
|
||||
name="Full Payload",
|
||||
config=_private_config(mqtt_broker.port, "mesh"),
|
||||
scope={"messages": "all", "raw_packets": "all"},
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
payload = {
|
||||
"type": "CHAN",
|
||||
"conversation_key": "ch1",
|
||||
"channel_name": "#general",
|
||||
"text": "Alice: hello mqtt",
|
||||
"sender_name": "Alice",
|
||||
"sender_key": "ab" * 32,
|
||||
"sender_timestamp": 1700000000,
|
||||
"received_at": 1700000001,
|
||||
"paths": [{"path": "aabb", "received_at": 1700000001}],
|
||||
"outgoing": False,
|
||||
"acked": 2,
|
||||
}
|
||||
|
||||
manager = FanoutManager()
|
||||
with (
|
||||
patch("app.fanout.mqtt_base._broadcast_health"),
|
||||
patch("app.websocket.broadcast_success"),
|
||||
patch("app.websocket.broadcast_error"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
):
|
||||
try:
|
||||
await manager.load_from_db()
|
||||
await _wait_connected(manager, cfg["id"])
|
||||
|
||||
await manager.broadcast_message(payload)
|
||||
messages = await mqtt_broker.wait_for(1)
|
||||
finally:
|
||||
await manager.stop_all()
|
||||
|
||||
assert len(messages) == 1
|
||||
topic, body = messages[0]
|
||||
assert topic == "mesh/gm:ch1"
|
||||
assert body == payload
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_one_disabled_only_enabled_receives(self, mqtt_broker, integration_db):
|
||||
"""Disabled integration must not publish any messages."""
|
||||
@@ -565,6 +613,44 @@ class TestFanoutWebhookIntegration:
|
||||
assert len(results) == 1
|
||||
assert results[0]["headers"].get("x-custom") == "my-value"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_webhook_preserves_full_message_payload(self, webhook_server, integration_db):
|
||||
"""Webhook delivers the full message payload body without dropping fields."""
|
||||
cfg = await FanoutConfigRepository.create(
|
||||
config_type="webhook",
|
||||
name="Full Payload Hook",
|
||||
config=_webhook_config(webhook_server.port),
|
||||
scope={"messages": "all", "raw_packets": "none"},
|
||||
enabled=True,
|
||||
)
|
||||
|
||||
payload = {
|
||||
"type": "CHAN",
|
||||
"conversation_key": "ch1",
|
||||
"channel_name": "#general",
|
||||
"text": "Alice: hello webhook",
|
||||
"sender_name": "Alice",
|
||||
"sender_key": "ab" * 32,
|
||||
"sender_timestamp": 1700000000,
|
||||
"received_at": 1700000001,
|
||||
"paths": [{"path": "aabb", "received_at": 1700000001}],
|
||||
"outgoing": False,
|
||||
"acked": 2,
|
||||
}
|
||||
|
||||
manager = FanoutManager()
|
||||
try:
|
||||
await manager.load_from_db()
|
||||
await _wait_connected(manager, cfg["id"])
|
||||
|
||||
await manager.broadcast_message(payload)
|
||||
results = await webhook_server.wait_for(1)
|
||||
finally:
|
||||
await manager.stop_all()
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0]["body"] == payload
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_webhook_hmac_signature(self, webhook_server, integration_db):
|
||||
"""Webhook sends HMAC-SHA256 signature when hmac_secret is configured."""
|
||||
|
||||
@@ -157,6 +157,7 @@ class TestOutgoingChannelBroadcast:
|
||||
assert data["type"] == "CHAN"
|
||||
assert data["conversation_key"] == chan_key.upper()
|
||||
assert data["sender_name"] == "MyNode"
|
||||
assert data["channel_name"] == "#general"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_channel_msg_response_includes_current_ack_count(self, test_db):
|
||||
@@ -177,6 +178,7 @@ class TestOutgoingChannelBroadcast:
|
||||
# Fresh message has acked=0
|
||||
assert message.id is not None
|
||||
assert message.acked == 0
|
||||
assert message.channel_name == "#acked"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_channel_msg_includes_sender_key(self, test_db):
|
||||
@@ -498,6 +500,7 @@ class TestResendChannelMessage:
|
||||
assert event_type == "message"
|
||||
assert event_data["id"] == result["message_id"]
|
||||
assert event_data["outgoing"] is True
|
||||
assert event_data["channel_name"] == "#broadcast"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_resend_byte_perfect_still_enforces_window(self, test_db):
|
||||
|
||||
Reference in New Issue
Block a user