mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-08 01:33:01 +02:00
Add MQTT removal migration and fix tests + docs
This commit is contained in:
@@ -31,7 +31,7 @@ test.describe('Apprise integration settings', () => {
|
||||
await page.getByRole('button', { name: 'Apprise' }).click();
|
||||
|
||||
// Should navigate to the detail/edit view with default name
|
||||
await expect(page.getByDisplayValue('Apprise')).toBeVisible();
|
||||
await expect(page.locator('#fanout-edit-name')).toHaveValue('Apprise');
|
||||
|
||||
// Fill in notification URL
|
||||
const urlsTextarea = page.locator('#fanout-apprise-urls');
|
||||
@@ -135,7 +135,7 @@ test.describe('Apprise integration settings', () => {
|
||||
await page.getByText('All except listed channels/contacts').click();
|
||||
|
||||
// Should show channel and contact lists with exclude label
|
||||
await expect(page.getByText('(exclude)')).toBeVisible();
|
||||
await expect(page.getByText('Channels (exclude)')).toBeVisible();
|
||||
|
||||
// Go back
|
||||
await page.getByText('← Back to list').click();
|
||||
@@ -158,9 +158,9 @@ test.describe('Apprise integration settings', () => {
|
||||
await page.getByText('Settings').click();
|
||||
await page.getByRole('button', { name: /MQTT.*Forwarding/ }).click();
|
||||
|
||||
// Should show "Disabled" text
|
||||
// Should show "Disabled" status text
|
||||
const row = page.getByText('Disabled Apprise').locator('..');
|
||||
await expect(row.getByText('Disabled')).toBeVisible();
|
||||
await expect(row.getByText('Disabled', { exact: true })).toBeVisible();
|
||||
|
||||
// Edit it
|
||||
await row.getByRole('button', { name: 'Edit' }).click();
|
||||
|
||||
@@ -31,7 +31,7 @@ test.describe('Webhook integration settings', () => {
|
||||
await page.getByRole('button', { name: 'Webhook' }).click();
|
||||
|
||||
// Should navigate to the detail/edit view with default name
|
||||
await expect(page.getByDisplayValue('Webhook')).toBeVisible();
|
||||
await expect(page.locator('#fanout-edit-name')).toHaveValue('Webhook');
|
||||
|
||||
// Fill in webhook URL
|
||||
const urlInput = page.locator('#fanout-webhook-url');
|
||||
@@ -85,7 +85,7 @@ test.describe('Webhook integration settings', () => {
|
||||
await row.getByRole('button', { name: 'Edit' }).click();
|
||||
|
||||
// Should be in edit view
|
||||
await expect(page.getByDisplayValue('API Webhook')).toBeVisible();
|
||||
await expect(page.locator('#fanout-edit-name')).toHaveValue('API Webhook');
|
||||
|
||||
// Change method to PUT
|
||||
await page.locator('#fanout-webhook-method').selectOption('PUT');
|
||||
@@ -129,9 +129,8 @@ test.describe('Webhook integration settings', () => {
|
||||
// Select "Only listed" to see channel/contact checkboxes
|
||||
await page.getByText('Only listed channels/contacts').click();
|
||||
|
||||
// Should show Channels and Contacts sections
|
||||
await expect(page.getByText('Channels')).toBeVisible();
|
||||
await expect(page.getByText('Contacts')).toBeVisible();
|
||||
// Should show Channels section (Contacts only appears if non-repeater contacts exist)
|
||||
await expect(page.getByText('Channels (include)')).toBeVisible();
|
||||
|
||||
// Go back without saving
|
||||
await page.getByText('← Back to list').click();
|
||||
|
||||
@@ -79,7 +79,6 @@ class TestDMAckTrackingWiring:
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.routers.messages.track_pending_ack") as mock_track,
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
patch("app.bot.run_bot_for_message", new=AsyncMock()),
|
||||
):
|
||||
request = SendDirectMessageRequest(destination=pub_key, text="Hello")
|
||||
message = await send_direct_message(request)
|
||||
@@ -112,7 +111,6 @@ class TestDMAckTrackingWiring:
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.routers.messages.track_pending_ack") as mock_track,
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
patch("app.bot.run_bot_for_message", new=AsyncMock()),
|
||||
):
|
||||
request = SendDirectMessageRequest(destination=pub_key, text="Hello")
|
||||
message = await send_direct_message(request)
|
||||
@@ -142,7 +140,6 @@ class TestDMAckTrackingWiring:
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.routers.messages.track_pending_ack") as mock_track,
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
patch("app.bot.run_bot_for_message", new=AsyncMock()),
|
||||
):
|
||||
request = SendDirectMessageRequest(destination=pub_key, text="Hello")
|
||||
await send_direct_message(request)
|
||||
@@ -171,7 +168,6 @@ class TestDMAckTrackingWiring:
|
||||
patch.object(radio_manager, "_meshcore", mc),
|
||||
patch("app.routers.messages.track_pending_ack") as mock_track,
|
||||
patch("app.routers.messages.broadcast_event"),
|
||||
patch("app.bot.run_bot_for_message", new=AsyncMock()),
|
||||
):
|
||||
request = SendDirectMessageRequest(destination=pub_key, text="Hello")
|
||||
message = await send_direct_message(request)
|
||||
|
||||
+21
-354
@@ -5,15 +5,12 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import app.bot as bot_module
|
||||
from app.bot import (
|
||||
import app.fanout.bot_exec as bot_module
|
||||
from app.fanout.bot_exec import (
|
||||
BOT_MESSAGE_SPACING,
|
||||
_bot_semaphore,
|
||||
execute_bot_code,
|
||||
process_bot_response,
|
||||
run_bot_for_message,
|
||||
)
|
||||
from app.models import BotConfig
|
||||
|
||||
|
||||
class TestExecuteBotCode:
|
||||
@@ -414,336 +411,6 @@ def bot(sender_name, sender_key, message_text, is_dm, channel_key, channel_name,
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestRunBotForMessage:
|
||||
"""Test the main bot entry point."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_semaphore(self):
|
||||
"""Reset semaphore state between tests."""
|
||||
# Ensure semaphore is fully released
|
||||
while _bot_semaphore.locked():
|
||||
_bot_semaphore.release()
|
||||
yield
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runs_for_outgoing_messages(self):
|
||||
"""Bot is triggered for outgoing messages (user can trigger their own bots)."""
|
||||
with patch("app.repository.AppSettingsRepository") as mock_repo:
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.bots = [
|
||||
BotConfig(id="1", name="Echo", enabled=True, code="def bot(**k): return 'echo'")
|
||||
]
|
||||
mock_repo.get = AsyncMock(return_value=mock_settings)
|
||||
|
||||
with (
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock),
|
||||
patch("app.bot.execute_bot_code", return_value="echo") as mock_exec,
|
||||
patch("app.bot.process_bot_response", new_callable=AsyncMock),
|
||||
):
|
||||
await run_bot_for_message(
|
||||
sender_name="Me",
|
||||
sender_key="abc123" + "0" * 58,
|
||||
message_text="Hello",
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
is_outgoing=True,
|
||||
)
|
||||
|
||||
# Bot should actually execute for outgoing messages
|
||||
mock_exec.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skips_when_no_enabled_bots(self):
|
||||
"""Bot is not triggered when no bots are enabled."""
|
||||
with patch("app.repository.AppSettingsRepository") as mock_repo:
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.bots = [
|
||||
BotConfig(id="1", name="Bot 1", enabled=False, code="def bot(): pass")
|
||||
]
|
||||
mock_repo.get = AsyncMock(return_value=mock_settings)
|
||||
|
||||
with patch("app.bot.execute_bot_code") as mock_exec:
|
||||
await run_bot_for_message(
|
||||
sender_name="Alice",
|
||||
sender_key="abc123",
|
||||
message_text="Hello",
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
)
|
||||
|
||||
mock_exec.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skips_when_bots_array_empty(self):
|
||||
"""Bot is not triggered when bots array is empty."""
|
||||
with patch("app.repository.AppSettingsRepository") as mock_repo:
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.bots = []
|
||||
mock_repo.get = AsyncMock(return_value=mock_settings)
|
||||
|
||||
with patch("app.bot.execute_bot_code") as mock_exec:
|
||||
await run_bot_for_message(
|
||||
sender_name="Alice",
|
||||
sender_key="abc123",
|
||||
message_text="Hello",
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
)
|
||||
|
||||
mock_exec.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_skips_bot_with_empty_code(self):
|
||||
"""Bot with empty code is skipped even if enabled."""
|
||||
with patch("app.repository.AppSettingsRepository") as mock_repo:
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.bots = [
|
||||
BotConfig(id="1", name="Empty Bot", enabled=True, code=""),
|
||||
BotConfig(id="2", name="Whitespace Bot", enabled=True, code=" "),
|
||||
]
|
||||
mock_repo.get = AsyncMock(return_value=mock_settings)
|
||||
|
||||
with patch("app.bot.execute_bot_code") as mock_exec:
|
||||
await run_bot_for_message(
|
||||
sender_name="Alice",
|
||||
sender_key="abc123",
|
||||
message_text="Hello",
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
)
|
||||
|
||||
mock_exec.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rechecks_settings_after_sleep(self):
|
||||
"""Settings are re-checked after 2 second sleep."""
|
||||
with patch("app.repository.AppSettingsRepository") as mock_repo:
|
||||
# First call: bot enabled
|
||||
# Second call (after sleep): bot disabled
|
||||
mock_settings_enabled = MagicMock()
|
||||
mock_settings_enabled.bots = [
|
||||
BotConfig(id="1", name="Bot 1", enabled=True, code="def bot(): return 'hi'")
|
||||
]
|
||||
|
||||
mock_settings_disabled = MagicMock()
|
||||
mock_settings_disabled.bots = [
|
||||
BotConfig(id="1", name="Bot 1", enabled=False, code="def bot(): return 'hi'")
|
||||
]
|
||||
|
||||
mock_repo.get = AsyncMock(side_effect=[mock_settings_enabled, mock_settings_disabled])
|
||||
|
||||
with (
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
||||
patch("app.bot.execute_bot_code") as mock_exec,
|
||||
):
|
||||
await run_bot_for_message(
|
||||
sender_name="Alice",
|
||||
sender_key="abc123",
|
||||
message_text="Hello",
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
)
|
||||
|
||||
# Should have slept
|
||||
mock_sleep.assert_called_once_with(2)
|
||||
|
||||
# Should NOT have executed bot (disabled after sleep)
|
||||
mock_exec.assert_not_called()
|
||||
|
||||
|
||||
class TestMultipleBots:
|
||||
"""Test multiple bots functionality."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_semaphore(self):
|
||||
"""Reset semaphore state between tests."""
|
||||
while _bot_semaphore.locked():
|
||||
_bot_semaphore.release()
|
||||
yield
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_rate_limit_state(self):
|
||||
"""Reset rate limiting state between tests."""
|
||||
bot_module._last_bot_send_time = 0.0
|
||||
yield
|
||||
bot_module._last_bot_send_time = 0.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_bots_execute_serially(self):
|
||||
"""Multiple enabled bots execute serially in order."""
|
||||
executed_bots = []
|
||||
|
||||
def mock_execute(code, *args, **kwargs):
|
||||
# Extract bot identifier from the code
|
||||
if "Bot 1" in code:
|
||||
executed_bots.append("Bot 1")
|
||||
return "Response 1"
|
||||
elif "Bot 2" in code:
|
||||
executed_bots.append("Bot 2")
|
||||
return "Response 2"
|
||||
return None
|
||||
|
||||
with patch("app.repository.AppSettingsRepository") as mock_repo:
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.bots = [
|
||||
BotConfig(id="1", name="Bot 1", enabled=True, code="# Bot 1\ndef bot(): pass"),
|
||||
BotConfig(id="2", name="Bot 2", enabled=True, code="# Bot 2\ndef bot(): pass"),
|
||||
]
|
||||
mock_repo.get = AsyncMock(return_value=mock_settings)
|
||||
|
||||
with (
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock),
|
||||
patch("app.bot.execute_bot_code", side_effect=mock_execute),
|
||||
patch("app.bot.process_bot_response", new_callable=AsyncMock),
|
||||
):
|
||||
await run_bot_for_message(
|
||||
sender_name="Alice",
|
||||
sender_key="abc123" + "0" * 58,
|
||||
message_text="Hello",
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
)
|
||||
|
||||
# Both bots should have executed in order
|
||||
assert executed_bots == ["Bot 1", "Bot 2"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disabled_bots_are_skipped(self):
|
||||
"""Disabled bots in the array are skipped."""
|
||||
executed_bots = []
|
||||
|
||||
def mock_execute(code, *args, **kwargs):
|
||||
if "Bot 1" in code:
|
||||
executed_bots.append("Bot 1")
|
||||
elif "Bot 2" in code:
|
||||
executed_bots.append("Bot 2")
|
||||
elif "Bot 3" in code:
|
||||
executed_bots.append("Bot 3")
|
||||
return None
|
||||
|
||||
with patch("app.repository.AppSettingsRepository") as mock_repo:
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.bots = [
|
||||
BotConfig(id="1", name="Bot 1", enabled=True, code="# Bot 1\ndef bot(): pass"),
|
||||
BotConfig(id="2", name="Bot 2", enabled=False, code="# Bot 2\ndef bot(): pass"),
|
||||
BotConfig(id="3", name="Bot 3", enabled=True, code="# Bot 3\ndef bot(): pass"),
|
||||
]
|
||||
mock_repo.get = AsyncMock(return_value=mock_settings)
|
||||
|
||||
with (
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock),
|
||||
patch("app.bot.execute_bot_code", side_effect=mock_execute),
|
||||
):
|
||||
await run_bot_for_message(
|
||||
sender_name="Alice",
|
||||
sender_key="abc123" + "0" * 58,
|
||||
message_text="Hello",
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
)
|
||||
|
||||
# Only enabled bots should have executed
|
||||
assert executed_bots == ["Bot 1", "Bot 3"]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_error_in_one_bot_doesnt_stop_others(self):
|
||||
"""Error in one bot doesn't prevent other bots from running."""
|
||||
executed_bots = []
|
||||
|
||||
def mock_execute(code, *args, **kwargs):
|
||||
if "Bot 1" in code:
|
||||
executed_bots.append("Bot 1")
|
||||
raise ValueError("Bot 1 crashed!")
|
||||
elif "Bot 2" in code:
|
||||
executed_bots.append("Bot 2")
|
||||
return "Response 2"
|
||||
elif "Bot 3" in code:
|
||||
executed_bots.append("Bot 3")
|
||||
return "Response 3"
|
||||
return None
|
||||
|
||||
with patch("app.repository.AppSettingsRepository") as mock_repo:
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.bots = [
|
||||
BotConfig(id="1", name="Bot 1", enabled=True, code="# Bot 1\ndef bot(): pass"),
|
||||
BotConfig(id="2", name="Bot 2", enabled=True, code="# Bot 2\ndef bot(): pass"),
|
||||
BotConfig(id="3", name="Bot 3", enabled=True, code="# Bot 3\ndef bot(): pass"),
|
||||
]
|
||||
mock_repo.get = AsyncMock(return_value=mock_settings)
|
||||
|
||||
with (
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock),
|
||||
patch("app.bot.execute_bot_code", side_effect=mock_execute),
|
||||
patch("app.bot.process_bot_response", new_callable=AsyncMock) as mock_respond,
|
||||
):
|
||||
await run_bot_for_message(
|
||||
sender_name="Alice",
|
||||
sender_key="abc123" + "0" * 58,
|
||||
message_text="Hello",
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
)
|
||||
|
||||
# All bots should have been attempted
|
||||
assert executed_bots == ["Bot 1", "Bot 2", "Bot 3"]
|
||||
|
||||
# Responses from successful bots should have been sent
|
||||
assert mock_respond.call_count == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_in_one_bot_doesnt_stop_others(self):
|
||||
"""Timeout in one bot doesn't prevent other bots from running."""
|
||||
executed_bots = []
|
||||
|
||||
async def mock_wait_for(coro, timeout):
|
||||
result = await coro
|
||||
# Simulate timeout for Bot 2
|
||||
if len(executed_bots) == 2 and executed_bots[-1] == "Bot 2":
|
||||
raise asyncio.TimeoutError()
|
||||
return result
|
||||
|
||||
def mock_execute(code, *args, **kwargs):
|
||||
if "Bot 1" in code:
|
||||
executed_bots.append("Bot 1")
|
||||
return "Response 1"
|
||||
elif "Bot 2" in code:
|
||||
executed_bots.append("Bot 2")
|
||||
return "Response 2" # This will be "timed out"
|
||||
elif "Bot 3" in code:
|
||||
executed_bots.append("Bot 3")
|
||||
return "Response 3"
|
||||
return None
|
||||
|
||||
with patch("app.repository.AppSettingsRepository") as mock_repo:
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.bots = [
|
||||
BotConfig(id="1", name="Bot 1", enabled=True, code="# Bot 1\ndef bot(): pass"),
|
||||
BotConfig(id="2", name="Bot 2", enabled=True, code="# Bot 2\ndef bot(): pass"),
|
||||
BotConfig(id="3", name="Bot 3", enabled=True, code="# Bot 3\ndef bot(): pass"),
|
||||
]
|
||||
mock_repo.get = AsyncMock(return_value=mock_settings)
|
||||
|
||||
with (
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock),
|
||||
patch("app.bot.execute_bot_code", side_effect=mock_execute),
|
||||
patch("app.bot.asyncio.wait_for", side_effect=mock_wait_for),
|
||||
patch("app.bot.process_bot_response", new_callable=AsyncMock) as mock_respond,
|
||||
):
|
||||
await run_bot_for_message(
|
||||
sender_name="Alice",
|
||||
sender_key="abc123" + "0" * 58,
|
||||
message_text="Hello",
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
)
|
||||
|
||||
# All bots should have been attempted
|
||||
assert executed_bots == ["Bot 1", "Bot 2", "Bot 3"]
|
||||
|
||||
# Only responses from non-timed-out bots (Bot 1 and Bot 3)
|
||||
assert mock_respond.call_count == 2
|
||||
|
||||
|
||||
class TestBotCodeValidation:
|
||||
"""Test bot code syntax validation via fanout router."""
|
||||
|
||||
@@ -804,8 +471,8 @@ class TestBotMessageRateLimiting:
|
||||
async def test_first_send_does_not_wait(self):
|
||||
"""First bot send should not wait (no previous send)."""
|
||||
with (
|
||||
patch("app.bot.time.monotonic", return_value=100.0),
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
||||
patch("app.fanout.bot_exec.time.monotonic", return_value=100.0),
|
||||
patch("app.fanout.bot_exec.asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
||||
patch("app.routers.messages.send_direct_message", new_callable=AsyncMock) as mock_send,
|
||||
patch("app.websocket.broadcast_event"),
|
||||
):
|
||||
@@ -832,8 +499,8 @@ class TestBotMessageRateLimiting:
|
||||
bot_module._last_bot_send_time = 100.0
|
||||
|
||||
with (
|
||||
patch("app.bot.time.monotonic", return_value=100.5),
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
||||
patch("app.fanout.bot_exec.time.monotonic", return_value=100.5),
|
||||
patch("app.fanout.bot_exec.asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
||||
patch("app.routers.messages.send_direct_message", new_callable=AsyncMock) as mock_send,
|
||||
patch("app.websocket.broadcast_event"),
|
||||
):
|
||||
@@ -860,8 +527,8 @@ class TestBotMessageRateLimiting:
|
||||
bot_module._last_bot_send_time = 97.0
|
||||
|
||||
with (
|
||||
patch("app.bot.time.monotonic", return_value=100.0),
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
||||
patch("app.fanout.bot_exec.time.monotonic", return_value=100.0),
|
||||
patch("app.fanout.bot_exec.asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
||||
patch("app.routers.messages.send_direct_message", new_callable=AsyncMock) as mock_send,
|
||||
patch("app.websocket.broadcast_event"),
|
||||
):
|
||||
@@ -883,7 +550,7 @@ class TestBotMessageRateLimiting:
|
||||
async def test_timestamp_updated_after_successful_send(self):
|
||||
"""Last send timestamp should be updated after successful send."""
|
||||
with (
|
||||
patch("app.bot.time.monotonic", return_value=150.0),
|
||||
patch("app.fanout.bot_exec.time.monotonic", return_value=150.0),
|
||||
patch("app.routers.messages.send_direct_message", new_callable=AsyncMock) as mock_send,
|
||||
patch("app.websocket.broadcast_event"),
|
||||
):
|
||||
@@ -908,7 +575,7 @@ class TestBotMessageRateLimiting:
|
||||
bot_module._last_bot_send_time = 50.0 # Previous timestamp
|
||||
|
||||
with (
|
||||
patch("app.bot.time.monotonic", return_value=100.0),
|
||||
patch("app.fanout.bot_exec.time.monotonic", return_value=100.0),
|
||||
patch(
|
||||
"app.routers.messages.send_direct_message",
|
||||
new_callable=AsyncMock,
|
||||
@@ -930,7 +597,7 @@ class TestBotMessageRateLimiting:
|
||||
"""Last send timestamp should NOT be updated if no destination."""
|
||||
bot_module._last_bot_send_time = 50.0
|
||||
|
||||
with patch("app.bot.time.monotonic", return_value=100.0):
|
||||
with patch("app.fanout.bot_exec.time.monotonic", return_value=100.0):
|
||||
await process_bot_response(
|
||||
response="Hello!",
|
||||
is_dm=False, # Not a DM
|
||||
@@ -964,8 +631,8 @@ class TestBotMessageRateLimiting:
|
||||
time_counter[0] += duration
|
||||
|
||||
with (
|
||||
patch("app.bot.time.monotonic", side_effect=mock_monotonic),
|
||||
patch("app.bot.asyncio.sleep", side_effect=mock_sleep),
|
||||
patch("app.fanout.bot_exec.time.monotonic", side_effect=mock_monotonic),
|
||||
patch("app.fanout.bot_exec.asyncio.sleep", side_effect=mock_sleep),
|
||||
patch("app.routers.messages.send_direct_message", side_effect=mock_send),
|
||||
patch("app.websocket.broadcast_event"),
|
||||
):
|
||||
@@ -990,8 +657,8 @@ class TestBotMessageRateLimiting:
|
||||
bot_module._last_bot_send_time = 99.0 # 1 second ago
|
||||
|
||||
with (
|
||||
patch("app.bot.time.monotonic", return_value=100.0),
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
||||
patch("app.fanout.bot_exec.time.monotonic", return_value=100.0),
|
||||
patch("app.fanout.bot_exec.asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
||||
patch("app.routers.messages.send_channel_message", new_callable=AsyncMock) as mock_send,
|
||||
patch("app.websocket.broadcast_event"),
|
||||
):
|
||||
@@ -1035,8 +702,8 @@ class TestBotListResponses:
|
||||
return mock_message
|
||||
|
||||
with (
|
||||
patch("app.bot.time.monotonic", return_value=100.0),
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock),
|
||||
patch("app.fanout.bot_exec.time.monotonic", return_value=100.0),
|
||||
patch("app.fanout.bot_exec.asyncio.sleep", new_callable=AsyncMock),
|
||||
patch("app.routers.messages.send_direct_message", side_effect=mock_send),
|
||||
patch("app.websocket.broadcast_event"),
|
||||
):
|
||||
@@ -1069,8 +736,8 @@ class TestBotListResponses:
|
||||
return mock_message
|
||||
|
||||
with (
|
||||
patch("app.bot.time.monotonic", side_effect=mock_monotonic),
|
||||
patch("app.bot.asyncio.sleep", side_effect=mock_sleep),
|
||||
patch("app.fanout.bot_exec.time.monotonic", side_effect=mock_monotonic),
|
||||
patch("app.fanout.bot_exec.asyncio.sleep", side_effect=mock_sleep),
|
||||
patch("app.routers.messages.send_direct_message", side_effect=mock_send),
|
||||
patch("app.websocket.broadcast_event"),
|
||||
):
|
||||
@@ -1098,8 +765,8 @@ class TestBotListResponses:
|
||||
return mock_message
|
||||
|
||||
with (
|
||||
patch("app.bot.time.monotonic", return_value=100.0),
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock),
|
||||
patch("app.fanout.bot_exec.time.monotonic", return_value=100.0),
|
||||
patch("app.fanout.bot_exec.asyncio.sleep", new_callable=AsyncMock),
|
||||
patch("app.routers.messages.send_direct_message", side_effect=mock_send),
|
||||
patch("app.websocket.broadcast_event"),
|
||||
):
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import json
|
||||
import time
|
||||
from contextlib import asynccontextmanager
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import nacl.bindings
|
||||
import pytest
|
||||
|
||||
from app.community_mqtt import (
|
||||
from app.fanout.community_mqtt import (
|
||||
_CLIENT_ID,
|
||||
_DEFAULT_BROKER,
|
||||
_STATS_REFRESH_INTERVAL,
|
||||
@@ -22,7 +23,6 @@ from app.community_mqtt import (
|
||||
_generate_jwt_token,
|
||||
_get_client_version,
|
||||
)
|
||||
from app.models import AppSettings
|
||||
|
||||
|
||||
def _make_test_keys() -> tuple[bytes, bytes]:
|
||||
@@ -49,6 +49,19 @@ def _make_test_keys() -> tuple[bytes, bytes]:
|
||||
return private_key, public_key
|
||||
|
||||
|
||||
def _make_community_settings(**overrides) -> SimpleNamespace:
|
||||
"""Create a settings namespace with all community MQTT fields."""
|
||||
defaults = {
|
||||
"community_mqtt_enabled": True,
|
||||
"community_mqtt_broker_host": "mqtt-us-v1.letsmesh.net",
|
||||
"community_mqtt_broker_port": 443,
|
||||
"community_mqtt_iata": "",
|
||||
"community_mqtt_email": "",
|
||||
}
|
||||
defaults.update(overrides)
|
||||
return SimpleNamespace(**defaults)
|
||||
|
||||
|
||||
class TestBase64UrlEncode:
|
||||
def test_encodes_without_padding(self):
|
||||
result = _base64url_encode(b"\x00\x01\x02")
|
||||
@@ -376,19 +389,19 @@ class TestCommunityMqttPublisher:
|
||||
|
||||
def test_is_configured_false_when_disabled(self):
|
||||
pub = CommunityMqttPublisher()
|
||||
pub._settings = AppSettings(community_mqtt_enabled=False)
|
||||
pub._settings = SimpleNamespace(community_mqtt_enabled=False)
|
||||
with patch("app.keystore.has_private_key", return_value=True):
|
||||
assert pub._is_configured() is False
|
||||
|
||||
def test_is_configured_false_when_no_private_key(self):
|
||||
pub = CommunityMqttPublisher()
|
||||
pub._settings = AppSettings(community_mqtt_enabled=True)
|
||||
pub._settings = SimpleNamespace(community_mqtt_enabled=True)
|
||||
with patch("app.keystore.has_private_key", return_value=False):
|
||||
assert pub._is_configured() is False
|
||||
|
||||
def test_is_configured_true_when_enabled_with_key(self):
|
||||
pub = CommunityMqttPublisher()
|
||||
pub._settings = AppSettings(community_mqtt_enabled=True)
|
||||
pub._settings = SimpleNamespace(community_mqtt_enabled=True)
|
||||
with patch("app.keystore.has_private_key", return_value=True):
|
||||
assert pub._is_configured() is True
|
||||
|
||||
@@ -408,12 +421,12 @@ class TestPublishFailureSetsDisconnected:
|
||||
|
||||
class TestBuildStatusTopic:
|
||||
def test_builds_correct_topic(self):
|
||||
settings = AppSettings(community_mqtt_iata="LAX")
|
||||
settings = SimpleNamespace(community_mqtt_iata="LAX")
|
||||
topic = _build_status_topic(settings, "AABB1122")
|
||||
assert topic == "meshcore/LAX/AABB1122/status"
|
||||
|
||||
def test_iata_uppercased_and_stripped(self):
|
||||
settings = AppSettings(community_mqtt_iata=" lax ")
|
||||
settings = SimpleNamespace(community_mqtt_iata=" lax ")
|
||||
topic = _build_status_topic(settings, "PUBKEY")
|
||||
assert topic == "meshcore/LAX/PUBKEY/status"
|
||||
|
||||
@@ -424,10 +437,7 @@ class TestLwtAndStatusPublish:
|
||||
pub = CommunityMqttPublisher()
|
||||
private_key, public_key = _make_test_keys()
|
||||
pubkey_hex = public_key.hex().upper()
|
||||
settings = AppSettings(
|
||||
community_mqtt_enabled=True,
|
||||
community_mqtt_iata="SFO",
|
||||
)
|
||||
settings = _make_community_settings(community_mqtt_iata="SFO")
|
||||
|
||||
mock_radio = MagicMock()
|
||||
mock_radio.meshcore = MagicMock()
|
||||
@@ -457,7 +467,7 @@ class TestLwtAndStatusPublish:
|
||||
pub = CommunityMqttPublisher()
|
||||
private_key, public_key = _make_test_keys()
|
||||
pubkey_hex = public_key.hex().upper()
|
||||
settings = AppSettings(
|
||||
settings = SimpleNamespace(
|
||||
community_mqtt_enabled=True,
|
||||
community_mqtt_iata="LAX",
|
||||
)
|
||||
@@ -478,8 +488,8 @@ class TestLwtAndStatusPublish:
|
||||
patch.object(
|
||||
pub, "_fetch_stats", new_callable=AsyncMock, return_value={"battery_mv": 4200}
|
||||
),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="915.0,250.0,10,8"),
|
||||
patch("app.community_mqtt._get_client_version", return_value="RemoteTerm 2.4.0"),
|
||||
patch("app.fanout.community_mqtt._build_radio_info", return_value="915.0,250.0,10,8"),
|
||||
patch("app.fanout.community_mqtt._get_client_version", return_value="RemoteTerm 2.4.0"),
|
||||
patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish,
|
||||
):
|
||||
await pub._on_connected_async(settings)
|
||||
@@ -507,10 +517,7 @@ class TestLwtAndStatusPublish:
|
||||
pub = CommunityMqttPublisher()
|
||||
private_key, public_key = _make_test_keys()
|
||||
pubkey_hex = public_key.hex().upper()
|
||||
settings = AppSettings(
|
||||
community_mqtt_enabled=True,
|
||||
community_mqtt_iata="JFK",
|
||||
)
|
||||
settings = _make_community_settings(community_mqtt_iata="JFK")
|
||||
|
||||
mock_radio = MagicMock()
|
||||
mock_radio.meshcore = None
|
||||
@@ -530,7 +537,7 @@ class TestLwtAndStatusPublish:
|
||||
async def test_on_connected_async_skips_when_no_public_key(self):
|
||||
"""_on_connected_async should no-op when public key is unavailable."""
|
||||
pub = CommunityMqttPublisher()
|
||||
settings = AppSettings(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
settings = SimpleNamespace(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
|
||||
with (
|
||||
patch("app.keystore.get_public_key", return_value=None),
|
||||
@@ -545,7 +552,7 @@ class TestLwtAndStatusPublish:
|
||||
"""Should use 'MeshCore Device' when radio name is unavailable."""
|
||||
pub = CommunityMqttPublisher()
|
||||
_, public_key = _make_test_keys()
|
||||
settings = AppSettings(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
settings = SimpleNamespace(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
|
||||
mock_radio = MagicMock()
|
||||
mock_radio.meshcore = None
|
||||
@@ -560,8 +567,10 @@ class TestLwtAndStatusPublish:
|
||||
return_value={"model": "unknown", "firmware_version": "unknown"},
|
||||
),
|
||||
patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=None),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="0,0,0,0"),
|
||||
patch("app.community_mqtt._get_client_version", return_value="RemoteTerm unknown"),
|
||||
patch("app.fanout.community_mqtt._build_radio_info", return_value="0,0,0,0"),
|
||||
patch(
|
||||
"app.fanout.community_mqtt._get_client_version", return_value="RemoteTerm unknown"
|
||||
),
|
||||
patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish,
|
||||
):
|
||||
await pub._on_connected_async(settings)
|
||||
@@ -844,14 +853,15 @@ class TestGetClientVersion:
|
||||
|
||||
def test_returns_version_from_metadata(self):
|
||||
"""Should use importlib.metadata to get version."""
|
||||
with patch("app.community_mqtt.importlib.metadata.version", return_value="1.2.3"):
|
||||
with patch("app.fanout.community_mqtt.importlib.metadata.version", return_value="1.2.3"):
|
||||
result = _get_client_version()
|
||||
assert result == "RemoteTerm 1.2.3"
|
||||
|
||||
def test_fallback_on_error(self):
|
||||
"""Should return 'RemoteTerm unknown' if metadata lookup fails."""
|
||||
with patch(
|
||||
"app.community_mqtt.importlib.metadata.version", side_effect=Exception("not found")
|
||||
"app.fanout.community_mqtt.importlib.metadata.version",
|
||||
side_effect=Exception("not found"),
|
||||
):
|
||||
result = _get_client_version()
|
||||
assert result == "RemoteTerm unknown"
|
||||
@@ -864,7 +874,7 @@ class TestPublishStatus:
|
||||
pub = CommunityMqttPublisher()
|
||||
_, public_key = _make_test_keys()
|
||||
pubkey_hex = public_key.hex().upper()
|
||||
settings = AppSettings(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
settings = SimpleNamespace(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
|
||||
mock_radio = MagicMock()
|
||||
mock_radio.meshcore = MagicMock()
|
||||
@@ -882,8 +892,8 @@ class TestPublishStatus:
|
||||
return_value={"model": "T-Deck", "firmware_version": "v2.2.2 (Build: 2025-01-15)"},
|
||||
),
|
||||
patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=stats),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="915.0,250.0,10,8"),
|
||||
patch("app.community_mqtt._get_client_version", return_value="RemoteTerm 2.4.0"),
|
||||
patch("app.fanout.community_mqtt._build_radio_info", return_value="915.0,250.0,10,8"),
|
||||
patch("app.fanout.community_mqtt._get_client_version", return_value="RemoteTerm 2.4.0"),
|
||||
patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish,
|
||||
):
|
||||
await pub._publish_status(settings)
|
||||
@@ -904,7 +914,7 @@ class TestPublishStatus:
|
||||
"""Should not include 'stats' key when stats are None."""
|
||||
pub = CommunityMqttPublisher()
|
||||
_, public_key = _make_test_keys()
|
||||
settings = AppSettings(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
settings = SimpleNamespace(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
|
||||
mock_radio = MagicMock()
|
||||
mock_radio.meshcore = None
|
||||
@@ -919,8 +929,10 @@ class TestPublishStatus:
|
||||
return_value={"model": "unknown", "firmware_version": "unknown"},
|
||||
),
|
||||
patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=None),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="0,0,0,0"),
|
||||
patch("app.community_mqtt._get_client_version", return_value="RemoteTerm unknown"),
|
||||
patch("app.fanout.community_mqtt._build_radio_info", return_value="0,0,0,0"),
|
||||
patch(
|
||||
"app.fanout.community_mqtt._get_client_version", return_value="RemoteTerm unknown"
|
||||
),
|
||||
patch.object(pub, "publish", new_callable=AsyncMock) as mock_publish,
|
||||
):
|
||||
await pub._publish_status(settings)
|
||||
@@ -933,7 +945,7 @@ class TestPublishStatus:
|
||||
"""Should update _last_status_publish after publishing."""
|
||||
pub = CommunityMqttPublisher()
|
||||
_, public_key = _make_test_keys()
|
||||
settings = AppSettings(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
settings = SimpleNamespace(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
|
||||
mock_radio = MagicMock()
|
||||
mock_radio.meshcore = None
|
||||
@@ -950,8 +962,10 @@ class TestPublishStatus:
|
||||
return_value={"model": "unknown", "firmware_version": "unknown"},
|
||||
),
|
||||
patch.object(pub, "_fetch_stats", new_callable=AsyncMock, return_value=None),
|
||||
patch("app.community_mqtt._build_radio_info", return_value="0,0,0,0"),
|
||||
patch("app.community_mqtt._get_client_version", return_value="RemoteTerm unknown"),
|
||||
patch("app.fanout.community_mqtt._build_radio_info", return_value="0,0,0,0"),
|
||||
patch(
|
||||
"app.fanout.community_mqtt._get_client_version", return_value="RemoteTerm unknown"
|
||||
),
|
||||
patch.object(pub, "publish", new_callable=AsyncMock),
|
||||
):
|
||||
await pub._publish_status(settings)
|
||||
@@ -962,7 +976,7 @@ class TestPublishStatus:
|
||||
async def test_no_publish_key_returns_none(self):
|
||||
"""Should skip publish when public key is unavailable."""
|
||||
pub = CommunityMqttPublisher()
|
||||
settings = AppSettings(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
settings = SimpleNamespace(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
|
||||
with (
|
||||
patch("app.keystore.get_public_key", return_value=None),
|
||||
@@ -978,7 +992,7 @@ class TestPeriodicWake:
|
||||
async def test_skips_before_interval(self):
|
||||
"""Should not republish before _STATS_REFRESH_INTERVAL."""
|
||||
pub = CommunityMqttPublisher()
|
||||
pub._settings = AppSettings(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
pub._settings = SimpleNamespace(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
pub._last_status_publish = time.monotonic() # Just published
|
||||
|
||||
with patch.object(pub, "_publish_status", new_callable=AsyncMock) as mock_ps:
|
||||
@@ -990,7 +1004,7 @@ class TestPeriodicWake:
|
||||
async def test_publishes_after_interval(self):
|
||||
"""Should republish after _STATS_REFRESH_INTERVAL elapsed."""
|
||||
pub = CommunityMqttPublisher()
|
||||
pub._settings = AppSettings(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
pub._settings = SimpleNamespace(community_mqtt_enabled=True, community_mqtt_iata="LAX")
|
||||
pub._last_status_publish = time.monotonic() - _STATS_REFRESH_INTERVAL - 1
|
||||
|
||||
with patch.object(pub, "_publish_status", new_callable=AsyncMock) as mock_ps:
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
"""Tests for the --disable-bots (MESHCORE_DISABLE_BOTS) startup flag.
|
||||
|
||||
Verifies that when disable_bots=True:
|
||||
- run_bot_for_message() exits immediately without any work
|
||||
- POST /api/fanout with type=bot returns 403
|
||||
- Health endpoint includes bots_disabled=True
|
||||
"""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.bot import run_bot_for_message
|
||||
from app.config import Settings
|
||||
from app.models import BotConfig
|
||||
from app.routers.fanout import FanoutConfigCreate, create_fanout_config
|
||||
from app.routers.health import build_health_data
|
||||
|
||||
@@ -30,54 +27,6 @@ class TestDisableBotsConfig:
|
||||
assert s.disable_bots is True
|
||||
|
||||
|
||||
class TestDisableBotsBotExecution:
|
||||
"""Test that run_bot_for_message exits immediately when bots are disabled."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_returns_immediately_when_disabled(self):
|
||||
"""No settings load, no semaphore, no bot execution."""
|
||||
with patch("app.bot.server_settings", MagicMock(disable_bots=True)):
|
||||
with patch("app.repository.AppSettingsRepository") as mock_repo:
|
||||
mock_repo.get = AsyncMock()
|
||||
|
||||
await run_bot_for_message(
|
||||
sender_name="Alice",
|
||||
sender_key="ab" * 32,
|
||||
message_text="Hello",
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
)
|
||||
|
||||
# Should never even load settings
|
||||
mock_repo.get.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runs_normally_when_not_disabled(self):
|
||||
"""Bots execute normally when disable_bots is False."""
|
||||
with patch("app.bot.server_settings", MagicMock(disable_bots=False)):
|
||||
with patch("app.repository.AppSettingsRepository") as mock_repo:
|
||||
mock_settings = MagicMock()
|
||||
mock_settings.bots = [
|
||||
BotConfig(id="1", name="Echo", enabled=True, code="def bot(**k): return 'echo'")
|
||||
]
|
||||
mock_repo.get = AsyncMock(return_value=mock_settings)
|
||||
|
||||
with (
|
||||
patch("app.bot.asyncio.sleep", new_callable=AsyncMock),
|
||||
patch("app.bot.execute_bot_code", return_value="echo") as mock_exec,
|
||||
patch("app.bot.process_bot_response", new_callable=AsyncMock),
|
||||
):
|
||||
await run_bot_for_message(
|
||||
sender_name="Alice",
|
||||
sender_key="ab" * 32,
|
||||
message_text="Hello",
|
||||
is_dm=True,
|
||||
channel_key=None,
|
||||
)
|
||||
|
||||
mock_exec.assert_called_once()
|
||||
|
||||
|
||||
class TestDisableBotsFanoutEndpoint:
|
||||
"""Test that bot creation via fanout router is rejected when bots are disabled."""
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""Tests for fanout bus: manager, scope matching, repository, and modules."""
|
||||
|
||||
import json
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -394,260 +393,6 @@ class TestBroadcastEventRealtime:
|
||||
mock_fm.broadcast_message.assert_called_once()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Migration test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _create_app_settings_table_sql():
|
||||
"""SQL to create app_settings with all MQTT columns for migration testing."""
|
||||
return """
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
max_radio_contacts INTEGER DEFAULT 200,
|
||||
favorites TEXT DEFAULT '[]',
|
||||
auto_decrypt_dm_on_advert INTEGER DEFAULT 0,
|
||||
sidebar_sort_order TEXT DEFAULT 'recent',
|
||||
last_message_times TEXT DEFAULT '{}',
|
||||
preferences_migrated INTEGER DEFAULT 0,
|
||||
advert_interval INTEGER DEFAULT 0,
|
||||
last_advert_time INTEGER DEFAULT 0,
|
||||
bots TEXT DEFAULT '[]',
|
||||
mqtt_broker_host TEXT DEFAULT '',
|
||||
mqtt_broker_port INTEGER DEFAULT 1883,
|
||||
mqtt_username TEXT DEFAULT '',
|
||||
mqtt_password TEXT DEFAULT '',
|
||||
mqtt_use_tls INTEGER DEFAULT 0,
|
||||
mqtt_tls_insecure INTEGER DEFAULT 0,
|
||||
mqtt_topic_prefix TEXT DEFAULT 'meshcore',
|
||||
mqtt_publish_messages INTEGER DEFAULT 0,
|
||||
mqtt_publish_raw_packets INTEGER DEFAULT 0,
|
||||
community_mqtt_enabled INTEGER DEFAULT 0,
|
||||
community_mqtt_iata TEXT DEFAULT '',
|
||||
community_mqtt_broker_host TEXT DEFAULT 'mqtt-us-v1.letsmesh.net',
|
||||
community_mqtt_broker_port INTEGER DEFAULT 443,
|
||||
community_mqtt_email TEXT DEFAULT '',
|
||||
flood_scope TEXT DEFAULT '',
|
||||
blocked_keys TEXT DEFAULT '[]',
|
||||
blocked_names TEXT DEFAULT '[]'
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
class TestMigration036:
|
||||
@pytest.mark.asyncio
|
||||
async def test_fanout_configs_table_created(self):
|
||||
"""Migration 36 should create the fanout_configs table."""
|
||||
from app.migrations import _migrate_036_create_fanout_configs
|
||||
|
||||
db = Database(":memory:")
|
||||
await db.connect()
|
||||
|
||||
await db.conn.execute(_create_app_settings_table_sql())
|
||||
await db.conn.execute("INSERT OR IGNORE INTO app_settings (id) VALUES (1)")
|
||||
await db.conn.commit()
|
||||
|
||||
try:
|
||||
await _migrate_036_create_fanout_configs(db.conn)
|
||||
|
||||
cursor = await db.conn.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='fanout_configs'"
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
assert row is not None
|
||||
finally:
|
||||
await db.disconnect()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migration_creates_mqtt_private_from_settings(self):
|
||||
"""Migration should create mqtt_private config from existing MQTT settings."""
|
||||
from app.migrations import _migrate_036_create_fanout_configs
|
||||
|
||||
db = Database(":memory:")
|
||||
await db.connect()
|
||||
|
||||
await db.conn.execute(_create_app_settings_table_sql())
|
||||
await db.conn.execute(
|
||||
"""INSERT OR REPLACE INTO app_settings (id, mqtt_broker_host, mqtt_broker_port,
|
||||
mqtt_username, mqtt_password, mqtt_use_tls, mqtt_tls_insecure,
|
||||
mqtt_topic_prefix, mqtt_publish_messages, mqtt_publish_raw_packets)
|
||||
VALUES (1, 'broker.local', 1883, 'user', 'pass', 0, 0, 'mesh', 1, 0)"""
|
||||
)
|
||||
await db.conn.commit()
|
||||
|
||||
try:
|
||||
await _migrate_036_create_fanout_configs(db.conn)
|
||||
|
||||
cursor = await db.conn.execute(
|
||||
"SELECT * FROM fanout_configs WHERE type = 'mqtt_private'"
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
assert row is not None
|
||||
|
||||
config = json.loads(row["config"])
|
||||
assert config["broker_host"] == "broker.local"
|
||||
assert config["username"] == "user"
|
||||
|
||||
scope = json.loads(row["scope"])
|
||||
assert scope["messages"] == "all"
|
||||
assert scope["raw_packets"] == "none"
|
||||
finally:
|
||||
await db.disconnect()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migration_creates_community_from_settings(self):
|
||||
"""Migration should create mqtt_community config when community was enabled."""
|
||||
from app.migrations import _migrate_036_create_fanout_configs
|
||||
|
||||
db = Database(":memory:")
|
||||
await db.connect()
|
||||
|
||||
await db.conn.execute(_create_app_settings_table_sql())
|
||||
await db.conn.execute(
|
||||
"""INSERT OR REPLACE INTO app_settings (id, community_mqtt_enabled, community_mqtt_iata,
|
||||
community_mqtt_broker_host, community_mqtt_broker_port, community_mqtt_email)
|
||||
VALUES (1, 1, 'DEN', 'mqtt-us-v1.letsmesh.net', 443, 'test@example.com')"""
|
||||
)
|
||||
await db.conn.commit()
|
||||
|
||||
try:
|
||||
await _migrate_036_create_fanout_configs(db.conn)
|
||||
|
||||
cursor = await db.conn.execute(
|
||||
"SELECT * FROM fanout_configs WHERE type = 'mqtt_community'"
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
assert row is not None
|
||||
assert bool(row["enabled"])
|
||||
|
||||
config = json.loads(row["config"])
|
||||
assert config["iata"] == "DEN"
|
||||
assert config["email"] == "test@example.com"
|
||||
finally:
|
||||
await db.disconnect()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migration_skips_when_no_mqtt_configured(self):
|
||||
"""Migration should not create rows when MQTT was not configured."""
|
||||
from app.migrations import _migrate_036_create_fanout_configs
|
||||
|
||||
db = Database(":memory:")
|
||||
await db.connect()
|
||||
|
||||
await db.conn.execute(_create_app_settings_table_sql())
|
||||
await db.conn.execute("INSERT OR IGNORE INTO app_settings (id) VALUES (1)")
|
||||
await db.conn.commit()
|
||||
|
||||
try:
|
||||
await _migrate_036_create_fanout_configs(db.conn)
|
||||
|
||||
cursor = await db.conn.execute("SELECT COUNT(*) FROM fanout_configs")
|
||||
row = await cursor.fetchone()
|
||||
assert row[0] == 0
|
||||
finally:
|
||||
await db.disconnect()
|
||||
|
||||
|
||||
async def _setup_db_with_fanout_table():
|
||||
"""Create a DB with app_settings + fanout_configs tables for migration 37 tests."""
|
||||
from app.migrations import _migrate_036_create_fanout_configs
|
||||
|
||||
db = Database(":memory:")
|
||||
await db.connect()
|
||||
|
||||
await db.conn.execute(_create_app_settings_table_sql())
|
||||
await db.conn.execute("INSERT OR IGNORE INTO app_settings (id) VALUES (1)")
|
||||
await db.conn.commit()
|
||||
await _migrate_036_create_fanout_configs(db.conn)
|
||||
return db
|
||||
|
||||
|
||||
class TestMigration037:
|
||||
@pytest.mark.asyncio
|
||||
async def test_migration_creates_bot_from_settings(self):
|
||||
"""Migration should create a fanout_configs row for each bot in app_settings."""
|
||||
from app.migrations import _migrate_037_bots_to_fanout
|
||||
|
||||
db = await _setup_db_with_fanout_table()
|
||||
try:
|
||||
bots_json = json.dumps(
|
||||
[
|
||||
{
|
||||
"id": "bot-1",
|
||||
"name": "EchoBot",
|
||||
"enabled": True,
|
||||
"code": "def bot(**k): return 'echo'",
|
||||
},
|
||||
{
|
||||
"id": "bot-2",
|
||||
"name": "Quiet",
|
||||
"enabled": False,
|
||||
"code": "def bot(**k): pass",
|
||||
},
|
||||
]
|
||||
)
|
||||
await db.conn.execute("UPDATE app_settings SET bots = ? WHERE id = 1", (bots_json,))
|
||||
await db.conn.commit()
|
||||
|
||||
await _migrate_037_bots_to_fanout(db.conn)
|
||||
|
||||
cursor = await db.conn.execute(
|
||||
"SELECT * FROM fanout_configs WHERE type = 'bot' ORDER BY sort_order"
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
assert len(rows) == 2
|
||||
|
||||
# First bot
|
||||
assert rows[0]["name"] == "EchoBot"
|
||||
assert bool(rows[0]["enabled"])
|
||||
config0 = json.loads(rows[0]["config"])
|
||||
assert config0["code"] == "def bot(**k): return 'echo'"
|
||||
scope0 = json.loads(rows[0]["scope"])
|
||||
assert scope0["messages"] == "all"
|
||||
assert scope0["raw_packets"] == "none"
|
||||
assert rows[0]["sort_order"] == 200
|
||||
|
||||
# Second bot
|
||||
assert rows[1]["name"] == "Quiet"
|
||||
assert not bool(rows[1]["enabled"])
|
||||
assert rows[1]["sort_order"] == 201
|
||||
finally:
|
||||
await db.disconnect()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migration_skips_when_no_bots(self):
|
||||
"""Migration should not create rows when there are no bots."""
|
||||
from app.migrations import _migrate_037_bots_to_fanout
|
||||
|
||||
db = await _setup_db_with_fanout_table()
|
||||
try:
|
||||
await _migrate_037_bots_to_fanout(db.conn)
|
||||
|
||||
cursor = await db.conn.execute("SELECT COUNT(*) FROM fanout_configs WHERE type = 'bot'")
|
||||
row = await cursor.fetchone()
|
||||
assert row[0] == 0
|
||||
finally:
|
||||
await db.disconnect()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migration_handles_empty_bots_array(self):
|
||||
"""Migration handles bots=[] gracefully."""
|
||||
from app.migrations import _migrate_037_bots_to_fanout
|
||||
|
||||
db = await _setup_db_with_fanout_table()
|
||||
try:
|
||||
await db.conn.execute("UPDATE app_settings SET bots = '[]' WHERE id = 1")
|
||||
await db.conn.commit()
|
||||
|
||||
await _migrate_037_bots_to_fanout(db.conn)
|
||||
|
||||
cursor = await db.conn.execute("SELECT COUNT(*) FROM fanout_configs WHERE type = 'bot'")
|
||||
row = await cursor.fetchone()
|
||||
assert row[0] == 0
|
||||
finally:
|
||||
await db.disconnect()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Webhook module unit tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -174,7 +174,7 @@ class TestFanoutMqttIntegration:
|
||||
|
||||
manager = FanoutManager()
|
||||
with (
|
||||
patch("app.mqtt_base._broadcast_health"),
|
||||
patch("app.fanout.mqtt_base._broadcast_health"),
|
||||
patch("app.websocket.broadcast_success"),
|
||||
patch("app.websocket.broadcast_error"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
@@ -218,7 +218,7 @@ class TestFanoutMqttIntegration:
|
||||
|
||||
manager = FanoutManager()
|
||||
with (
|
||||
patch("app.mqtt_base._broadcast_health"),
|
||||
patch("app.fanout.mqtt_base._broadcast_health"),
|
||||
patch("app.websocket.broadcast_success"),
|
||||
patch("app.websocket.broadcast_error"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
@@ -264,7 +264,7 @@ class TestFanoutMqttIntegration:
|
||||
|
||||
manager = FanoutManager()
|
||||
with (
|
||||
patch("app.mqtt_base._broadcast_health"),
|
||||
patch("app.fanout.mqtt_base._broadcast_health"),
|
||||
patch("app.websocket.broadcast_success"),
|
||||
patch("app.websocket.broadcast_error"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
@@ -297,7 +297,7 @@ class TestFanoutMqttIntegration:
|
||||
|
||||
manager = FanoutManager()
|
||||
with (
|
||||
patch("app.mqtt_base._broadcast_health"),
|
||||
patch("app.fanout.mqtt_base._broadcast_health"),
|
||||
patch("app.websocket.broadcast_success"),
|
||||
patch("app.websocket.broadcast_error"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
@@ -345,7 +345,7 @@ class TestFanoutMqttIntegration:
|
||||
|
||||
manager = FanoutManager()
|
||||
with (
|
||||
patch("app.mqtt_base._broadcast_health"),
|
||||
patch("app.fanout.mqtt_base._broadcast_health"),
|
||||
patch("app.websocket.broadcast_success"),
|
||||
patch("app.websocket.broadcast_error"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
@@ -382,7 +382,7 @@ class TestFanoutMqttIntegration:
|
||||
|
||||
manager = FanoutManager()
|
||||
with (
|
||||
patch("app.mqtt_base._broadcast_health"),
|
||||
patch("app.fanout.mqtt_base._broadcast_health"),
|
||||
patch("app.websocket.broadcast_success"),
|
||||
patch("app.websocket.broadcast_error"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
|
||||
+41
-49
@@ -100,8 +100,8 @@ class TestMigration001:
|
||||
# Run migrations
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 37 # All migrations run
|
||||
assert await get_version(conn) == 37
|
||||
assert applied == 38 # All migrations run
|
||||
assert await get_version(conn) == 38
|
||||
|
||||
# Verify columns exist by inserting and selecting
|
||||
await conn.execute(
|
||||
@@ -183,9 +183,9 @@ class TestMigration001:
|
||||
applied1 = await run_migrations(conn)
|
||||
applied2 = await run_migrations(conn)
|
||||
|
||||
assert applied1 == 37 # All migrations run
|
||||
assert applied1 == 38 # All migrations run
|
||||
assert applied2 == 0 # No migrations on second run
|
||||
assert await get_version(conn) == 37
|
||||
assert await get_version(conn) == 38
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -246,8 +246,8 @@ class TestMigration001:
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
# All migrations applied (version incremented) but no error
|
||||
assert applied == 37
|
||||
assert await get_version(conn) == 37
|
||||
assert applied == 38
|
||||
assert await get_version(conn) == 38
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -374,28 +374,27 @@ class TestMigration013:
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
# Run migration 13 (plus 14-37 which also run)
|
||||
# Run migration 13 (plus 14-38 which also run)
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 25
|
||||
assert await get_version(conn) == 37
|
||||
assert applied == 26
|
||||
assert await get_version(conn) == 38
|
||||
|
||||
# Verify bots array was created with migrated data
|
||||
cursor = await conn.execute("SELECT bots FROM app_settings WHERE id = 1")
|
||||
# Bots were migrated from app_settings to fanout_configs (migration 37)
|
||||
# and the bots column was dropped (migration 38)
|
||||
cursor = await conn.execute("SELECT * FROM fanout_configs WHERE type = 'bot'")
|
||||
row = await cursor.fetchone()
|
||||
bots = json.loads(row["bots"])
|
||||
assert row is not None
|
||||
|
||||
assert len(bots) == 1
|
||||
assert bots[0]["name"] == "Bot 1"
|
||||
assert bots[0]["enabled"] is True
|
||||
assert bots[0]["code"] == 'def bot(): return "hello"'
|
||||
assert "id" in bots[0] # Should have a UUID
|
||||
config = json.loads(row["config"])
|
||||
assert config["code"] == 'def bot(): return "hello"'
|
||||
assert row["name"] == "Bot 1"
|
||||
assert bool(row["enabled"])
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migration_creates_empty_array_when_no_bot(self):
|
||||
"""Migration creates empty bots array when no existing bot data."""
|
||||
import json
|
||||
|
||||
conn = await aiosqlite.connect(":memory:")
|
||||
conn.row_factory = aiosqlite.Row
|
||||
@@ -424,11 +423,10 @@ class TestMigration013:
|
||||
|
||||
await run_migrations(conn)
|
||||
|
||||
cursor = await conn.execute("SELECT bots FROM app_settings WHERE id = 1")
|
||||
# Bots column was dropped by migration 38; verify no bots in fanout_configs
|
||||
cursor = await conn.execute("SELECT COUNT(*) FROM fanout_configs WHERE type = 'bot'")
|
||||
row = await cursor.fetchone()
|
||||
bots = json.loads(row["bots"])
|
||||
|
||||
assert bots == []
|
||||
assert row[0] == 0
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -497,7 +495,7 @@ class TestMigration018:
|
||||
assert await cursor.fetchone() is not None
|
||||
|
||||
await run_migrations(conn)
|
||||
assert await get_version(conn) == 37
|
||||
assert await get_version(conn) == 38
|
||||
|
||||
# Verify autoindex is gone
|
||||
cursor = await conn.execute(
|
||||
@@ -575,8 +573,8 @@ class TestMigration018:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 20 # Migrations 18-37 run (18+19 skip internally)
|
||||
assert await get_version(conn) == 37
|
||||
assert applied == 21 # Migrations 18-38 run (18+19 skip internally)
|
||||
assert await get_version(conn) == 38
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -648,7 +646,7 @@ class TestMigration019:
|
||||
assert await cursor.fetchone() is not None
|
||||
|
||||
await run_migrations(conn)
|
||||
assert await get_version(conn) == 37
|
||||
assert await get_version(conn) == 38
|
||||
|
||||
# Verify autoindex is gone
|
||||
cursor = await conn.execute(
|
||||
@@ -714,8 +712,8 @@ class TestMigration020:
|
||||
assert (await cursor.fetchone())[0] == "delete"
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 18 # Migrations 20-37
|
||||
assert await get_version(conn) == 37
|
||||
assert applied == 19 # Migrations 20-38
|
||||
assert await get_version(conn) == 38
|
||||
|
||||
# Verify WAL mode
|
||||
cursor = await conn.execute("PRAGMA journal_mode")
|
||||
@@ -745,7 +743,7 @@ class TestMigration020:
|
||||
await set_version(conn, 20)
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 17 # Migrations 21-37 still run
|
||||
assert applied == 18 # Migrations 21-38 still run
|
||||
|
||||
# Still WAL + INCREMENTAL
|
||||
cursor = await conn.execute("PRAGMA journal_mode")
|
||||
@@ -803,8 +801,8 @@ class TestMigration028:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 10
|
||||
assert await get_version(conn) == 37
|
||||
assert applied == 11
|
||||
assert await get_version(conn) == 38
|
||||
|
||||
# Verify payload_hash column is now BLOB
|
||||
cursor = await conn.execute("PRAGMA table_info(raw_packets)")
|
||||
@@ -873,8 +871,8 @@ class TestMigration028:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 10 # Version still bumped
|
||||
assert await get_version(conn) == 37
|
||||
assert applied == 11 # Version still bumped
|
||||
assert await get_version(conn) == 38
|
||||
|
||||
# Verify data unchanged
|
||||
cursor = await conn.execute("SELECT payload_hash FROM raw_packets")
|
||||
@@ -923,22 +921,16 @@ class TestMigration032:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 6
|
||||
assert await get_version(conn) == 37
|
||||
assert applied == 7
|
||||
assert await get_version(conn) == 38
|
||||
|
||||
# Verify all columns exist with correct defaults
|
||||
# Community MQTT columns were added by migration 32 and dropped by migration 38.
|
||||
# Verify community settings were NOT migrated (no community config existed).
|
||||
cursor = await conn.execute(
|
||||
"""SELECT community_mqtt_enabled, community_mqtt_iata,
|
||||
community_mqtt_broker_host, community_mqtt_broker_port,
|
||||
community_mqtt_email
|
||||
FROM app_settings WHERE id = 1"""
|
||||
"SELECT COUNT(*) FROM fanout_configs WHERE type = 'mqtt_community'"
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
assert row["community_mqtt_enabled"] == 0
|
||||
assert row["community_mqtt_iata"] == ""
|
||||
assert row["community_mqtt_broker_host"] == "mqtt-us-v1.letsmesh.net"
|
||||
assert row["community_mqtt_broker_port"] == 443
|
||||
assert row["community_mqtt_email"] == ""
|
||||
assert row[0] == 0
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -996,8 +988,8 @@ class TestMigration034:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 4
|
||||
assert await get_version(conn) == 37
|
||||
assert applied == 5
|
||||
assert await get_version(conn) == 38
|
||||
|
||||
# Verify column exists with correct default
|
||||
cursor = await conn.execute("SELECT flood_scope FROM app_settings WHERE id = 1")
|
||||
@@ -1039,8 +1031,8 @@ class TestMigration033:
|
||||
await conn.commit()
|
||||
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 5
|
||||
assert await get_version(conn) == 37
|
||||
assert applied == 6
|
||||
assert await get_version(conn) == 38
|
||||
|
||||
cursor = await conn.execute(
|
||||
"SELECT key, name, is_hashtag, on_radio FROM channels WHERE key = ?",
|
||||
|
||||
+23
-22
@@ -1,28 +1,29 @@
|
||||
"""Tests for MQTT publisher module."""
|
||||
|
||||
import ssl
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.models import AppSettings
|
||||
from app.mqtt import MqttPublisher, _build_message_topic, _build_raw_packet_topic
|
||||
from app.fanout.mqtt import MqttPublisher, _build_message_topic, _build_raw_packet_topic
|
||||
|
||||
|
||||
def _make_settings(**overrides) -> AppSettings:
|
||||
"""Create an AppSettings with MQTT fields."""
|
||||
def _make_settings(**overrides) -> SimpleNamespace:
|
||||
"""Create a settings namespace with MQTT fields."""
|
||||
defaults = {
|
||||
"mqtt_broker_host": "broker.local",
|
||||
"mqtt_broker_port": 1883,
|
||||
"mqtt_username": "",
|
||||
"mqtt_password": "",
|
||||
"mqtt_use_tls": False,
|
||||
"mqtt_tls_insecure": False,
|
||||
"mqtt_topic_prefix": "meshcore",
|
||||
"mqtt_publish_messages": True,
|
||||
"mqtt_publish_raw_packets": True,
|
||||
}
|
||||
defaults.update(overrides)
|
||||
return AppSettings(**defaults)
|
||||
return SimpleNamespace(**defaults)
|
||||
|
||||
|
||||
class TestTopicBuilders:
|
||||
@@ -214,8 +215,8 @@ class TestConnectionLoop:
|
||||
mock_client.__aenter__ = AsyncMock(side_effect=side_effect_aenter)
|
||||
|
||||
with (
|
||||
patch("app.mqtt_base.aiomqtt.Client", return_value=mock_client),
|
||||
patch("app.mqtt_base._broadcast_health"),
|
||||
patch("app.fanout.mqtt_base.aiomqtt.Client", return_value=mock_client),
|
||||
patch("app.fanout.mqtt_base._broadcast_health"),
|
||||
patch("app.websocket.broadcast_success"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
):
|
||||
@@ -235,7 +236,7 @@ class TestConnectionLoop:
|
||||
"""Connection loop should retry after a connection error with backoff."""
|
||||
import asyncio
|
||||
|
||||
from app.mqtt_base import _BACKOFF_MIN
|
||||
from app.fanout.mqtt_base import _BACKOFF_MIN
|
||||
|
||||
pub = MqttPublisher()
|
||||
settings = _make_settings()
|
||||
@@ -268,12 +269,12 @@ class TestConnectionLoop:
|
||||
return factory
|
||||
|
||||
with (
|
||||
patch("app.mqtt_base.aiomqtt.Client", side_effect=make_client_factory()),
|
||||
patch("app.mqtt_base._broadcast_health"),
|
||||
patch("app.fanout.mqtt_base.aiomqtt.Client", side_effect=make_client_factory()),
|
||||
patch("app.fanout.mqtt_base._broadcast_health"),
|
||||
patch("app.websocket.broadcast_success"),
|
||||
patch("app.websocket.broadcast_error"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
patch("app.mqtt_base.asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
||||
patch("app.fanout.mqtt_base.asyncio.sleep", new_callable=AsyncMock) as mock_sleep,
|
||||
):
|
||||
await pub.start(settings)
|
||||
|
||||
@@ -292,7 +293,7 @@ class TestConnectionLoop:
|
||||
"""Backoff should double after each failure, capped at _backoff_max."""
|
||||
import asyncio
|
||||
|
||||
from app.mqtt_base import _BACKOFF_MIN
|
||||
from app.fanout.mqtt_base import _BACKOFF_MIN
|
||||
|
||||
pub = MqttPublisher()
|
||||
settings = _make_settings()
|
||||
@@ -322,11 +323,11 @@ class TestConnectionLoop:
|
||||
raise asyncio.CancelledError
|
||||
|
||||
with (
|
||||
patch("app.mqtt_base.aiomqtt.Client", side_effect=factory),
|
||||
patch("app.mqtt_base._broadcast_health"),
|
||||
patch("app.fanout.mqtt_base.aiomqtt.Client", side_effect=factory),
|
||||
patch("app.fanout.mqtt_base._broadcast_health"),
|
||||
patch("app.websocket.broadcast_error"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
patch("app.mqtt_base.asyncio.sleep", side_effect=capture_sleep),
|
||||
patch("app.fanout.mqtt_base.asyncio.sleep", side_effect=capture_sleep),
|
||||
):
|
||||
await pub.start(settings)
|
||||
try:
|
||||
@@ -363,8 +364,8 @@ class TestConnectionLoop:
|
||||
return mock
|
||||
|
||||
with (
|
||||
patch("app.mqtt_base.aiomqtt.Client", side_effect=make_success_client),
|
||||
patch("app.mqtt_base._broadcast_health"),
|
||||
patch("app.fanout.mqtt_base.aiomqtt.Client", side_effect=make_success_client),
|
||||
patch("app.fanout.mqtt_base._broadcast_health"),
|
||||
patch("app.websocket.broadcast_success"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
):
|
||||
@@ -411,8 +412,8 @@ class TestConnectionLoop:
|
||||
return mock
|
||||
|
||||
with (
|
||||
patch("app.mqtt_base.aiomqtt.Client", side_effect=make_client),
|
||||
patch("app.mqtt_base._broadcast_health", side_effect=track_health),
|
||||
patch("app.fanout.mqtt_base.aiomqtt.Client", side_effect=make_client),
|
||||
patch("app.fanout.mqtt_base._broadcast_health", side_effect=track_health),
|
||||
patch("app.websocket.broadcast_success"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
):
|
||||
@@ -448,11 +449,11 @@ class TestConnectionLoop:
|
||||
return mock
|
||||
|
||||
with (
|
||||
patch("app.mqtt_base.aiomqtt.Client", side_effect=make_failing_client),
|
||||
patch("app.mqtt_base._broadcast_health", side_effect=track_health),
|
||||
patch("app.fanout.mqtt_base.aiomqtt.Client", side_effect=make_failing_client),
|
||||
patch("app.fanout.mqtt_base._broadcast_health", side_effect=track_health),
|
||||
patch("app.websocket.broadcast_error"),
|
||||
patch("app.websocket.broadcast_health"),
|
||||
patch("app.mqtt_base.asyncio.sleep", side_effect=cancel_on_sleep),
|
||||
patch("app.fanout.mqtt_base.asyncio.sleep", side_effect=cancel_on_sleep),
|
||||
):
|
||||
await pub.start(settings)
|
||||
try:
|
||||
|
||||
@@ -492,21 +492,6 @@ class TestAppSettingsRepository:
|
||||
"preferences_migrated": 0,
|
||||
"advert_interval": None,
|
||||
"last_advert_time": None,
|
||||
"bots": "{bad-bots-json",
|
||||
"mqtt_broker_host": "",
|
||||
"mqtt_broker_port": 1883,
|
||||
"mqtt_username": "",
|
||||
"mqtt_password": "",
|
||||
"mqtt_use_tls": 0,
|
||||
"mqtt_tls_insecure": 0,
|
||||
"mqtt_topic_prefix": "meshcore",
|
||||
"mqtt_publish_messages": 0,
|
||||
"mqtt_publish_raw_packets": 0,
|
||||
"community_mqtt_enabled": 0,
|
||||
"community_mqtt_iata": "",
|
||||
"community_mqtt_broker_host": "mqtt-us-v1.letsmesh.net",
|
||||
"community_mqtt_broker_port": 443,
|
||||
"community_mqtt_email": "",
|
||||
"flood_scope": "",
|
||||
"blocked_keys": "[]",
|
||||
"blocked_names": "[]",
|
||||
@@ -525,7 +510,6 @@ class TestAppSettingsRepository:
|
||||
assert settings.favorites == []
|
||||
assert settings.last_message_times == {}
|
||||
assert settings.sidebar_sort_order == "recent"
|
||||
assert settings.bots == []
|
||||
assert settings.advert_interval == 0
|
||||
assert settings.last_advert_time == 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user