mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
remove radio dependency fallback shim
This commit is contained in:
@@ -1,21 +1,8 @@
|
||||
"""Shared dependencies for FastAPI routers."""
|
||||
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.services.radio_runtime import RadioRuntime
|
||||
from app.services.radio_runtime import radio_runtime as radio_manager
|
||||
|
||||
|
||||
def require_connected():
|
||||
"""Dependency that ensures radio is connected and returns meshcore instance.
|
||||
|
||||
Raises HTTPException 503 if radio is not connected.
|
||||
"""
|
||||
if isinstance(radio_manager, RadioRuntime):
|
||||
return radio_manager.require_connected()
|
||||
if getattr(radio_manager, "is_setup_in_progress", False) is True:
|
||||
raise HTTPException(status_code=503, detail="Radio is initializing")
|
||||
mc = getattr(radio_manager, "meshcore", None)
|
||||
if not getattr(radio_manager, "is_connected", False) or mc is None:
|
||||
raise HTTPException(status_code=503, detail="Radio not connected")
|
||||
return mc
|
||||
"""Dependency that ensures radio is connected and returns meshcore instance."""
|
||||
return radio_manager.require_connected()
|
||||
|
||||
@@ -9,6 +9,7 @@ import time
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from app.radio import radio_manager
|
||||
from app.repository import (
|
||||
@@ -29,6 +30,15 @@ def _reset_radio_state():
|
||||
radio_manager._operation_lock = prev_lock
|
||||
|
||||
|
||||
def _patch_require_connected(mc=None, *, detail="Radio not connected"):
|
||||
if mc is None:
|
||||
return patch(
|
||||
"app.dependencies.radio_manager.require_connected",
|
||||
side_effect=HTTPException(status_code=503, detail=detail),
|
||||
)
|
||||
return patch("app.dependencies.radio_manager.require_connected", return_value=mc)
|
||||
|
||||
|
||||
async def _insert_contact(public_key, name="Alice", **overrides):
|
||||
"""Insert a contact into the test database."""
|
||||
data = {
|
||||
@@ -102,10 +112,7 @@ class TestRadioDisconnectedHandler:
|
||||
|
||||
# require_connected() passes, but _meshcore is None when radio_operation() checks
|
||||
radio_manager._meshcore = None
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = MagicMock()
|
||||
|
||||
with _patch_require_connected(MagicMock()):
|
||||
response = await client.post(
|
||||
"/api/messages/direct", json={"destination": pub_key, "text": "Hi"}
|
||||
)
|
||||
@@ -120,10 +127,7 @@ class TestMessagesEndpoint:
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_direct_message_requires_connection(self, test_db, client):
|
||||
"""Sending message when disconnected returns 503."""
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = False
|
||||
mock_rm.meshcore = None
|
||||
|
||||
with _patch_require_connected():
|
||||
response = await client.post(
|
||||
"/api/messages/direct", json={"destination": "abc123", "text": "Hello"}
|
||||
)
|
||||
@@ -134,10 +138,7 @@ class TestMessagesEndpoint:
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_channel_message_requires_connection(self, test_db, client):
|
||||
"""Sending channel message when disconnected returns 503."""
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = False
|
||||
mock_rm.meshcore = None
|
||||
|
||||
with _patch_require_connected():
|
||||
response = await client.post(
|
||||
"/api/messages/channel",
|
||||
json={"channel_key": "0123456789ABCDEF0123456789ABCDEF", "text": "Hello"},
|
||||
@@ -164,12 +165,9 @@ class TestMessagesEndpoint:
|
||||
|
||||
radio_manager._meshcore = mock_mc
|
||||
with (
|
||||
patch("app.dependencies.radio_manager") as mock_rm,
|
||||
_patch_require_connected(mock_mc),
|
||||
patch("app.routers.messages.broadcast_event") as mock_broadcast,
|
||||
):
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
|
||||
response = await client.post(
|
||||
"/api/messages/direct",
|
||||
json={"destination": pub_key, "text": "Hello"},
|
||||
@@ -202,13 +200,10 @@ class TestMessagesEndpoint:
|
||||
|
||||
radio_manager._meshcore = mock_mc
|
||||
with (
|
||||
patch("app.dependencies.radio_manager") as mock_rm,
|
||||
_patch_require_connected(mock_mc),
|
||||
patch("app.decoder.calculate_channel_hash", return_value="abcd"),
|
||||
patch("app.routers.messages.broadcast_event") as mock_broadcast,
|
||||
):
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
|
||||
response = await client.post(
|
||||
"/api/messages/channel",
|
||||
json={"channel_key": chan_key, "text": "Hello room"},
|
||||
@@ -226,10 +221,7 @@ class TestMessagesEndpoint:
|
||||
mock_mc = MagicMock()
|
||||
mock_mc.get_contact_by_key_prefix.return_value = None
|
||||
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
|
||||
with _patch_require_connected(mock_mc):
|
||||
response = await client.post(
|
||||
"/api/messages/direct", json={"destination": "nonexistent", "text": "Hello"}
|
||||
)
|
||||
@@ -257,11 +249,9 @@ class TestMessagesEndpoint:
|
||||
|
||||
radio_manager._meshcore = mock_mc
|
||||
with (
|
||||
patch("app.dependencies.radio_manager") as mock_rm,
|
||||
_patch_require_connected(mock_mc),
|
||||
patch("app.routers.messages.MessageRepository") as mock_msg_repo,
|
||||
):
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
# Simulate duplicate - create returns None
|
||||
mock_msg_repo.create = AsyncMock(return_value=None)
|
||||
|
||||
@@ -294,11 +284,9 @@ class TestMessagesEndpoint:
|
||||
|
||||
radio_manager._meshcore = mock_mc
|
||||
with (
|
||||
patch("app.dependencies.radio_manager") as mock_rm,
|
||||
_patch_require_connected(mock_mc),
|
||||
patch("app.routers.messages.MessageRepository") as mock_msg_repo,
|
||||
):
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
# Simulate duplicate - create returns None
|
||||
mock_msg_repo.create = AsyncMock(return_value=None)
|
||||
|
||||
@@ -315,10 +303,7 @@ class TestMessagesEndpoint:
|
||||
@pytest.mark.asyncio
|
||||
async def test_resend_channel_message_requires_connection(self, test_db, client):
|
||||
"""Resend endpoint returns 503 when radio is disconnected."""
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = False
|
||||
mock_rm.meshcore = None
|
||||
|
||||
with _patch_require_connected():
|
||||
response = await client.post("/api/messages/channel/1/resend")
|
||||
|
||||
assert response.status_code == 503
|
||||
@@ -353,10 +338,7 @@ class TestMessagesEndpoint:
|
||||
)
|
||||
|
||||
radio_manager._meshcore = mock_mc
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
|
||||
with _patch_require_connected(mock_mc):
|
||||
response = await client.post(f"/api/messages/channel/{msg_id}/resend")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -394,10 +376,7 @@ class TestMessagesEndpoint:
|
||||
mock_mc.commands.set_channel = AsyncMock()
|
||||
mock_mc.commands.send_chan_msg = AsyncMock()
|
||||
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
|
||||
with _patch_require_connected(mock_mc):
|
||||
response = await client.post(f"/api/messages/channel/{msg_id}/resend")
|
||||
|
||||
assert response.status_code == 400
|
||||
@@ -414,10 +393,7 @@ class TestMessagesEndpoint:
|
||||
mock_mc.commands.set_channel = AsyncMock()
|
||||
mock_mc.commands.send_chan_msg = AsyncMock()
|
||||
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
|
||||
with _patch_require_connected(mock_mc):
|
||||
response = await client.post("/api/messages/channel/999999/resend")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@@ -9,6 +9,7 @@ from contextlib import asynccontextmanager
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from meshcore import EventType
|
||||
|
||||
from app.radio import radio_manager
|
||||
@@ -55,6 +56,15 @@ def _make_error_response():
|
||||
return result
|
||||
|
||||
|
||||
def _patch_require_connected(mc=None, *, detail="Radio not connected"):
|
||||
if mc is None:
|
||||
return patch(
|
||||
"app.dependencies.radio_manager.require_connected",
|
||||
side_effect=HTTPException(status_code=503, detail=detail),
|
||||
)
|
||||
return patch("app.dependencies.radio_manager.require_connected", return_value=mc)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def _noop_radio_operation(mc):
|
||||
"""No-op radio_operation context manager that yields mc."""
|
||||
@@ -83,11 +93,9 @@ class TestSyncChannelsFromRadio:
|
||||
radio_manager._meshcore = mock_mc
|
||||
|
||||
with (
|
||||
patch("app.dependencies.radio_manager") as mock_dep_rm,
|
||||
_patch_require_connected(mock_mc),
|
||||
patch("app.routers.channels.radio_manager") as mock_ch_rm,
|
||||
):
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
mock_ch_rm.radio_operation = lambda desc: _noop_radio_operation(mock_mc)
|
||||
|
||||
response = await client.post("/api/channels/sync?max_channels=5")
|
||||
@@ -119,11 +127,9 @@ class TestSyncChannelsFromRadio:
|
||||
radio_manager._meshcore = mock_mc
|
||||
|
||||
with (
|
||||
patch("app.dependencies.radio_manager") as mock_dep_rm,
|
||||
_patch_require_connected(mock_mc),
|
||||
patch("app.routers.channels.radio_manager") as mock_ch_rm,
|
||||
):
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
mock_ch_rm.radio_operation = lambda desc: _noop_radio_operation(mock_mc)
|
||||
|
||||
response = await client.post("/api/channels/sync?max_channels=5")
|
||||
@@ -146,11 +152,9 @@ class TestSyncChannelsFromRadio:
|
||||
radio_manager._meshcore = mock_mc
|
||||
|
||||
with (
|
||||
patch("app.dependencies.radio_manager") as mock_dep_rm,
|
||||
_patch_require_connected(mock_mc),
|
||||
patch("app.routers.channels.radio_manager") as mock_ch_rm,
|
||||
):
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
mock_ch_rm.radio_operation = lambda desc: _noop_radio_operation(mock_mc)
|
||||
|
||||
response = await client.post("/api/channels/sync?max_channels=3")
|
||||
@@ -178,11 +182,9 @@ class TestSyncChannelsFromRadio:
|
||||
radio_manager._meshcore = mock_mc
|
||||
|
||||
with (
|
||||
patch("app.dependencies.radio_manager") as mock_dep_rm,
|
||||
_patch_require_connected(mock_mc),
|
||||
patch("app.routers.channels.radio_manager") as mock_ch_rm,
|
||||
):
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
mock_ch_rm.radio_operation = lambda desc: _noop_radio_operation(mock_mc)
|
||||
|
||||
await client.post("/api/channels/sync?max_channels=3")
|
||||
@@ -193,10 +195,7 @@ class TestSyncChannelsFromRadio:
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_requires_connection(self, test_db, client):
|
||||
"""Sync returns 503 when radio is not connected."""
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = False
|
||||
mock_rm.meshcore = None
|
||||
|
||||
with _patch_require_connected():
|
||||
response = await client.post("/api/channels/sync")
|
||||
|
||||
assert response.status_code == 503
|
||||
@@ -216,11 +215,9 @@ class TestSyncChannelsFromRadio:
|
||||
radio_manager._meshcore = mock_mc
|
||||
|
||||
with (
|
||||
patch("app.dependencies.radio_manager") as mock_dep_rm,
|
||||
_patch_require_connected(mock_mc),
|
||||
patch("app.routers.channels.radio_manager") as mock_ch_rm,
|
||||
):
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
mock_ch_rm.radio_operation = lambda desc: _noop_radio_operation(mock_mc)
|
||||
|
||||
await client.post("/api/channels/sync?max_channels=3")
|
||||
@@ -246,11 +243,9 @@ class TestSyncChannelsFromRadio:
|
||||
radio_manager._meshcore = mock_mc
|
||||
|
||||
with (
|
||||
patch("app.dependencies.radio_manager") as mock_dep_rm,
|
||||
_patch_require_connected(mock_mc),
|
||||
patch("app.routers.channels.radio_manager") as mock_ch_rm,
|
||||
):
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
mock_ch_rm.radio_operation = lambda desc: _noop_radio_operation(mock_mc)
|
||||
|
||||
response = await client.post("/api/channels/sync?max_channels=3")
|
||||
|
||||
@@ -10,6 +10,7 @@ from contextlib import asynccontextmanager
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from meshcore import EventType
|
||||
|
||||
from app.radio import radio_manager
|
||||
@@ -31,6 +32,15 @@ def _noop_radio_operation(mc=None):
|
||||
return _ctx
|
||||
|
||||
|
||||
def _patch_require_connected(mc=None, *, detail="Radio not connected"):
|
||||
if mc is None:
|
||||
return patch(
|
||||
"app.dependencies.radio_manager.require_connected",
|
||||
side_effect=HTTPException(status_code=503, detail=detail),
|
||||
)
|
||||
return patch("app.dependencies.radio_manager.require_connected", return_value=mc)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_radio_state():
|
||||
"""Save/restore radio_manager state so tests don't leak."""
|
||||
@@ -505,10 +515,7 @@ class TestSyncContacts:
|
||||
mock_mc.commands.get_contacts = AsyncMock(return_value=mock_result)
|
||||
|
||||
radio_manager._meshcore = mock_mc
|
||||
with patch("app.dependencies.radio_manager") as mock_dep_rm:
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
|
||||
with _patch_require_connected(mock_mc):
|
||||
response = await client.post("/api/contacts/sync")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -521,10 +528,7 @@ class TestSyncContacts:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sync_requires_connection(self, test_db, client):
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = False
|
||||
mock_rm.meshcore = None
|
||||
|
||||
with _patch_require_connected():
|
||||
response = await client.post("/api/contacts/sync")
|
||||
|
||||
assert response.status_code == 503
|
||||
@@ -547,10 +551,7 @@ class TestSyncContacts:
|
||||
mock_mc.commands.get_contacts = AsyncMock(return_value=mock_result)
|
||||
|
||||
radio_manager._meshcore = mock_mc
|
||||
with patch("app.dependencies.radio_manager") as mock_dep_rm:
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
|
||||
with _patch_require_connected(mock_mc):
|
||||
response = await client.post("/api/contacts/sync")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -771,10 +772,7 @@ class TestAddRemoveRadio:
|
||||
mock_mc.commands.add_contact = AsyncMock(return_value=mock_result)
|
||||
|
||||
radio_manager._meshcore = mock_mc
|
||||
with patch("app.dependencies.radio_manager") as mock_dep_rm:
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
|
||||
with _patch_require_connected(mock_mc):
|
||||
response = await client.post(f"/api/contacts/{KEY_A}/add-to-radio")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -800,10 +798,7 @@ class TestAddRemoveRadio:
|
||||
mock_mc.commands.add_contact = AsyncMock(return_value=mock_result)
|
||||
|
||||
radio_manager._meshcore = mock_mc
|
||||
with patch("app.dependencies.radio_manager") as mock_dep_rm:
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
|
||||
with _patch_require_connected(mock_mc):
|
||||
response = await client.post(f"/api/contacts/{KEY_A}/add-to-radio")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -821,10 +816,7 @@ class TestAddRemoveRadio:
|
||||
mock_mc.get_contact_by_key_prefix = MagicMock(return_value=MagicMock()) # On radio
|
||||
|
||||
radio_manager._meshcore = mock_mc
|
||||
with patch("app.dependencies.radio_manager") as mock_dep_rm:
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
|
||||
with _patch_require_connected(mock_mc):
|
||||
response = await client.post(f"/api/contacts/{KEY_A}/add-to-radio")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -842,10 +834,7 @@ class TestAddRemoveRadio:
|
||||
mock_mc.commands.remove_contact = AsyncMock(return_value=mock_result)
|
||||
|
||||
radio_manager._meshcore = mock_mc
|
||||
with patch("app.dependencies.radio_manager") as mock_dep_rm:
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
|
||||
with _patch_require_connected(mock_mc):
|
||||
response = await client.post(f"/api/contacts/{KEY_A}/remove-from-radio")
|
||||
|
||||
assert response.status_code == 200
|
||||
@@ -857,10 +846,7 @@ class TestAddRemoveRadio:
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_requires_connection(self, test_db, client):
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = False
|
||||
mock_rm.meshcore = None
|
||||
|
||||
with _patch_require_connected():
|
||||
response = await client.post(f"/api/contacts/{KEY_A}/add-to-radio")
|
||||
|
||||
assert response.status_code == 503
|
||||
@@ -869,10 +855,7 @@ class TestAddRemoveRadio:
|
||||
async def test_remove_not_found(self, test_db, client):
|
||||
mock_mc = MagicMock()
|
||||
|
||||
with patch("app.dependencies.radio_manager") as mock_dep_rm:
|
||||
mock_dep_rm.is_connected = True
|
||||
mock_dep_rm.meshcore = mock_mc
|
||||
|
||||
with _patch_require_connected(mock_mc):
|
||||
response = await client.post(f"/api/contacts/{KEY_A}/remove-from-radio")
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
@@ -7,6 +7,11 @@ import pytest
|
||||
|
||||
from app.radio import RadioDisconnectedError, RadioOperationBusyError, radio_manager
|
||||
from app.radio_sync import is_polling_paused
|
||||
from app.services.radio_runtime import RadioRuntime
|
||||
|
||||
|
||||
def _runtime(manager):
|
||||
return RadioRuntime(lambda: manager)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -180,11 +185,11 @@ class TestRequireConnected:
|
||||
|
||||
from app.dependencies import require_connected
|
||||
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = MagicMock()
|
||||
mock_rm.is_setup_in_progress = True
|
||||
|
||||
manager = MagicMock()
|
||||
manager.is_connected = True
|
||||
manager.meshcore = MagicMock()
|
||||
manager.is_setup_in_progress = True
|
||||
with patch("app.dependencies.radio_manager", _runtime(manager)):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
require_connected()
|
||||
|
||||
@@ -197,11 +202,11 @@ class TestRequireConnected:
|
||||
|
||||
from app.dependencies import require_connected
|
||||
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_setup_in_progress = False
|
||||
mock_rm.is_connected = False
|
||||
mock_rm.meshcore = None
|
||||
|
||||
manager = MagicMock()
|
||||
manager.is_setup_in_progress = False
|
||||
manager.is_connected = False
|
||||
manager.meshcore = None
|
||||
with patch("app.dependencies.radio_manager", _runtime(manager)):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
require_connected()
|
||||
|
||||
@@ -212,11 +217,11 @@ class TestRequireConnected:
|
||||
from app.dependencies import require_connected
|
||||
|
||||
mock_mc = MagicMock()
|
||||
with patch("app.dependencies.radio_manager") as mock_rm:
|
||||
mock_rm.is_setup_in_progress = False
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
|
||||
manager = MagicMock()
|
||||
manager.is_setup_in_progress = False
|
||||
manager.is_connected = True
|
||||
manager.meshcore = mock_mc
|
||||
with patch("app.dependencies.radio_manager", _runtime(manager)):
|
||||
result = require_connected()
|
||||
|
||||
assert result is mock_mc
|
||||
|
||||
Reference in New Issue
Block a user