diff --git a/app/services/dm_ingest.py b/app/services/dm_ingest.py index f5a39d7..df01e46 100644 --- a/app/services/dm_ingest.py +++ b/app/services/dm_ingest.py @@ -132,20 +132,6 @@ async def resolve_direct_message_sender_metadata( ) return contact.name, contact.public_key.lower() - if normalized_sender: - placeholder_upsert = ContactUpsert( - public_key=normalized_sender, - type=0, - last_seen=received_at, - last_contacted=received_at, - first_seen=received_at, - on_radio=False, - ) - await contact_repository.upsert(placeholder_upsert) - placeholder = await contact_repository.get_by_key(normalized_sender) - if placeholder is not None: - broadcast_fn("contact", placeholder.model_dump()) - return None, normalized_sender or None diff --git a/frontend/src/components/RoomServerPanel.tsx b/frontend/src/components/RoomServerPanel.tsx index bb1bbe9..58c278c 100644 --- a/frontend/src/components/RoomServerPanel.tsx +++ b/frontend/src/components/RoomServerPanel.tsx @@ -138,7 +138,7 @@ export function RoomServerPanel({ contact, onAuthenticatedChange }: RoomServerPa setLoginMessage(null); try { const result = await api.roomLogin(contact.public_key, password); - setAuthenticated(result.authenticated); + setAuthenticated(true); setLoginMessage( result.message ?? (result.authenticated @@ -152,7 +152,7 @@ export function RoomServerPanel({ contact, onAuthenticatedChange }: RoomServerPa } } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; - setAuthenticated(false); + setAuthenticated(true); setLoginError(message); toast.error('Room login failed', { description: message }); } finally { diff --git a/frontend/src/test/roomServerPanel.test.tsx b/frontend/src/test/roomServerPanel.test.tsx new file mode 100644 index 0000000..bb9718a --- /dev/null +++ b/frontend/src/test/roomServerPanel.test.tsx @@ -0,0 +1,71 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'; + +import { RoomServerPanel } from '../components/RoomServerPanel'; +import type { Contact } from '../types'; + +vi.mock('../api', () => ({ + api: { + roomLogin: vi.fn(), + roomStatus: vi.fn(), + roomAcl: vi.fn(), + roomLppTelemetry: vi.fn(), + sendRepeaterCommand: vi.fn(), + }, +})); + +vi.mock('../components/ui/sonner', () => ({ + toast: Object.assign(vi.fn(), { + success: vi.fn(), + error: vi.fn(), + warning: vi.fn(), + }), +})); + +const { api: _rawApi } = await import('../api'); +const mockApi = _rawApi as unknown as Record; + +const roomContact: Contact = { + public_key: 'aa'.repeat(32), + name: 'Ops Board', + type: 3, + flags: 0, + direct_path: null, + direct_path_len: -1, + direct_path_hash_mode: 0, + last_advert: null, + lat: null, + lon: null, + last_seen: null, + on_radio: false, + last_contacted: null, + last_read_at: null, + first_seen: null, +}; + +describe('RoomServerPanel', () => { + beforeEach(() => { + vi.clearAllMocks(); + localStorage.clear(); + }); + + it('keeps room controls available when login is not confirmed', async () => { + mockApi.roomLogin.mockResolvedValueOnce({ + status: 'timeout', + authenticated: false, + message: + 'No login confirmation was heard from the room server. The control panel is still available; try logging in again if authenticated actions fail.', + }); + const onAuthenticatedChange = vi.fn(); + + render(); + + fireEvent.click(screen.getByText('Login with ACL / Guest')); + + await waitFor(() => { + expect(screen.getByText('Room Server Controls')).toBeInTheDocument(); + }); + expect(screen.getByText(/control panel is still available/i)).toBeInTheDocument(); + expect(onAuthenticatedChange).toHaveBeenLastCalledWith(true); + }); +}); diff --git a/tests/test_event_handlers.py b/tests/test_event_handlers.py index f0bacf8..5c810c0 100644 --- a/tests/test_event_handlers.py +++ b/tests/test_event_handlers.py @@ -492,6 +492,58 @@ class TestContactMessageCLIFiltering: assert payload["sender_key"] == author_key assert payload["signature"] == author_key[:8] + @pytest.mark.asyncio + async def test_room_server_message_does_not_create_placeholder_contact_for_unknown_author( + self, test_db + ): + from app.event_handlers import on_contact_message + + room_key = "ab" * 32 + author_prefix = "12345678" + await ContactRepository.upsert( + { + "public_key": room_key, + "name": "Ops Board", + "type": 3, + "flags": 0, + "direct_path": None, + "direct_path_len": -1, + "direct_path_hash_mode": -1, + "last_advert": None, + "lat": None, + "lon": None, + "last_seen": None, + "on_radio": False, + "last_contacted": None, + "first_seen": None, + } + ) + + with patch("app.event_handlers.broadcast_event") as mock_broadcast: + + class MockEvent: + payload = { + "pubkey_prefix": room_key[:12], + "text": "hello room", + "txt_type": 2, + "signature": author_prefix, + "sender_timestamp": 1700000000, + } + + await on_contact_message(MockEvent()) + + message = (await MessageRepository.get_all(msg_type="PRIV", conversation_key=room_key))[ + 0 + ] + assert message.sender_name is None + assert message.sender_key == author_prefix + assert await ContactRepository.get_by_key(author_prefix) is None + + assert len(mock_broadcast.call_args_list) == 1 + event_type, payload = mock_broadcast.call_args_list[0][0] + assert event_type == "message" + assert payload["sender_key"] == author_prefix + @pytest.mark.asyncio async def test_missing_txt_type_defaults_to_normal(self, test_db): """Messages without txt_type field are treated as normal (not filtered)."""