Don't log on missed login ack and don't make standalone contacts for repeater users

This commit is contained in:
Jack Kingsman
2026-03-19 20:26:10 -07:00
parent d05312c157
commit cee7103ec6
4 changed files with 125 additions and 16 deletions

View File

@@ -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

View File

@@ -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 {

View File

@@ -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<string, Mock>;
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(<RoomServerPanel contact={roomContact} onAuthenticatedChange={onAuthenticatedChange} />);
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);
});
});

View File

@@ -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)."""