diff --git a/app/routers/contacts.py b/app/routers/contacts.py index 99dcf05..e84c877 100644 --- a/app/routers/contacts.py +++ b/app/routers/contacts.py @@ -419,6 +419,7 @@ async def add_contact_to_radio(public_key: str) -> dict: # Check if already on radio radio_contact = mc.get_contact_by_key_prefix(contact.public_key[:12]) if radio_contact: + await ContactRepository.set_on_radio(contact.public_key, True) return {"status": "ok", "message": "Contact already on radio"} logger.info("Adding contact %s to radio", contact.public_key[:12]) diff --git a/app/services/message_send.py b/app/services/message_send.py index 2fd0d99..be15291 100644 --- a/app/services/message_send.py +++ b/app/services/message_send.py @@ -206,7 +206,6 @@ async def send_channel_message_to_channel( message_repository=MessageRepository, ) -> Any: """Send a channel message and persist/broadcast the outgoing row.""" - message_id: int | None = None now: int | None = None radio_name = "" our_public_key: str | None = None @@ -235,27 +234,27 @@ async def send_channel_message_to_channel( if result.type == EventType.ERROR: raise HTTPException(status_code=500, detail=f"Failed to send message: {result.payload}") - outgoing_message = await create_outgoing_channel_message( - conversation_key=channel_key_upper, - text=text_with_sender, - sender_timestamp=now, - received_at=now, - sender_name=radio_name or None, - sender_key=our_public_key, - channel_name=channel.name, - broadcast_fn=broadcast_fn, - message_repository=message_repository, - ) - if outgoing_message is None: - raise HTTPException( - status_code=500, - detail="Failed to store outgoing message - unexpected duplicate", - ) - message_id = outgoing_message.id - - if message_id is None or now is None: + if now is None: raise HTTPException(status_code=500, detail="Failed to store outgoing message") + outgoing_message = await create_outgoing_channel_message( + conversation_key=channel_key_upper, + text=text_with_sender, + sender_timestamp=now, + received_at=now, + sender_name=radio_name or None, + sender_key=our_public_key, + channel_name=channel.name, + broadcast_fn=broadcast_fn, + message_repository=message_repository, + ) + if outgoing_message is None: + raise HTTPException( + status_code=500, + detail="Failed to store outgoing message - unexpected duplicate", + ) + + message_id = outgoing_message.id acked_count, paths = await message_repository.get_ack_and_paths(message_id) return build_message_model( message_id=message_id, diff --git a/tests/test_contacts_router.py b/tests/test_contacts_router.py index 7e28625..9af9242 100644 --- a/tests/test_contacts_router.py +++ b/tests/test_contacts_router.py @@ -954,8 +954,8 @@ class TestAddRemoveRadio: @pytest.mark.asyncio async def test_add_already_on_radio(self, test_db, client): - """Adding a contact already on radio returns ok without calling add_contact.""" - await _insert_contact(KEY_A, on_radio=True) + """Adding a contact already on radio repairs the DB flag and skips add_contact.""" + await _insert_contact(KEY_A, on_radio=False) mock_mc = MagicMock() mock_mc.get_contact_by_key_prefix = MagicMock(return_value=MagicMock()) # On radio @@ -966,6 +966,10 @@ class TestAddRemoveRadio: assert response.status_code == 200 assert "already" in response.json()["message"].lower() + contact = await ContactRepository.get_by_key(KEY_A) + assert contact is not None + assert contact.on_radio is True + mock_mc.commands.add_contact.assert_not_called() @pytest.mark.asyncio async def test_remove_from_radio(self, test_db, client): diff --git a/tests/test_send_messages.py b/tests/test_send_messages.py index 3a6cb4e..0197105 100644 --- a/tests/test_send_messages.py +++ b/tests/test_send_messages.py @@ -907,3 +907,35 @@ class TestConcurrentChannelSends: msg_type="CHAN", conversation_key=chan_key.upper(), limit=10 ) assert len(msgs) == 2 + + +class TestChannelSendLockScope: + """Channel send should release the radio lock before DB persistence work.""" + + @pytest.mark.asyncio + async def test_channel_message_row_created_after_radio_lock_released(self, test_db): + mc = _make_mc(name="TestNode") + chan_key = "de" * 16 + await ChannelRepository.upsert(key=chan_key, name="#lockscope") + + observed_lock_states: list[bool] = [] + original_create = MessageRepository.create + + async def _assert_lock_then_create(*args, **kwargs): + observed_lock_states.append(bool(radio_manager._operation_lock.locked())) + return await original_create(*args, **kwargs) + + with ( + patch("app.routers.messages.require_connected", return_value=mc), + patch.object(radio_manager, "_meshcore", mc), + patch("app.routers.messages.broadcast_event"), + patch( + "app.services.message_send.MessageRepository.create", + side_effect=_assert_lock_then_create, + ), + ): + await send_channel_message( + SendChannelMessageRequest(channel_key=chan_key, text="Lock scope test") + ) + + assert observed_lock_states == [False]