meshcore_py/tests/unit/test_commands.py

334 lines
12 KiB
Python
Raw Normal View History

2025-04-13 22:55:31 -07:00
import pytest
import asyncio
from unittest.mock import MagicMock, AsyncMock
2025-04-13 22:55:31 -07:00
from meshcore.commands import CommandHandler
from meshcore.events import EventType, Event, Subscription
2025-04-13 22:55:31 -07:00
pytestmark = pytest.mark.asyncio
VALID_PUBKEY_HEX = "0123456789abcdef" * 4 # 64 hex chars = 32 bytes
2025-04-13 22:55:31 -07:00
# Fixtures
@pytest.fixture
def mock_connection():
connection = MagicMock()
connection.send = AsyncMock()
return connection
2025-04-13 22:55:31 -07:00
@pytest.fixture
def mock_dispatcher():
dispatcher = MagicMock()
dispatcher.wait_for_event = AsyncMock()
dispatcher.dispatch = AsyncMock()
def fake_subscribe(event_type, handler, attribute_filters=None):
sub = MagicMock(spec=Subscription)
sub.unsubscribe = MagicMock()
dispatcher._last_subscribe_handler = handler
dispatcher._last_subscribe_event_type = event_type
return sub
dispatcher.subscribe = MagicMock(side_effect=fake_subscribe)
2025-04-13 22:55:31 -07:00
return dispatcher
2025-04-13 22:55:31 -07:00
@pytest.fixture
def command_handler(mock_connection, mock_dispatcher):
handler = CommandHandler()
2025-04-13 22:55:31 -07:00
async def sender(data):
await mock_connection.send(data)
2025-04-13 22:55:31 -07:00
handler._sender_func = sender
2025-04-13 22:55:31 -07:00
handler.dispatcher = mock_dispatcher
return handler
2025-04-13 22:55:31 -07:00
def setup_event_response(mock_dispatcher, event_type, payload, attribute_filters=None):
def fake_subscribe(evt_type, handler, attr_filters=None):
sub = MagicMock(spec=Subscription)
sub.unsubscribe = MagicMock()
if evt_type == event_type:
asyncio.get_event_loop().call_soon(
handler, Event(event_type, payload)
)
return sub
mock_dispatcher.subscribe = MagicMock(side_effect=fake_subscribe)
2025-04-13 22:55:31 -07:00
2025-04-13 22:55:31 -07:00
# Basic tests
async def test_send_basic(command_handler, mock_connection):
result = await command_handler.send(b"test_data")
mock_connection.send.assert_called_once_with(b"test_data")
assert result.type == EventType.OK
assert result.payload == {}
2025-04-13 22:55:31 -07:00
2025-04-13 22:55:31 -07:00
async def test_send_with_event(command_handler, mock_connection, mock_dispatcher):
expected_payload = {"value": 42}
2025-04-13 22:55:31 -07:00
setup_event_response(mock_dispatcher, EventType.OK, expected_payload)
2025-04-13 22:55:31 -07:00
result = await command_handler.send(b"test_command", [EventType.OK])
2025-04-13 22:55:31 -07:00
mock_connection.send.assert_called_once_with(b"test_command")
assert result.type == EventType.OK
assert result.payload == expected_payload
2025-04-13 22:55:31 -07:00
2025-04-13 22:55:31 -07:00
async def test_send_timeout(command_handler, mock_connection, mock_dispatcher):
result = await command_handler.send(b"test_command", [EventType.OK], timeout=0.1)
assert result.type == EventType.ERROR
assert result.payload == {"reason": "no_event_received"}
2025-04-13 22:55:31 -07:00
2025-04-13 22:55:31 -07:00
# Destination validation tests
async def test_validate_destination_bytes(command_handler, mock_connection):
dst = b"123456789012" # 12 bytes
await command_handler.send_msg(dst, "test message")
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x00\x00")
assert b"123456" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
async def test_validate_destination_hex_string(command_handler, mock_connection):
dst = "0123456789abcdef"
2025-04-13 22:55:31 -07:00
await command_handler.send_msg(dst, "test message")
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x00\x00")
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
async def test_validate_destination_contact_object(command_handler, mock_connection):
dst = {"public_key": "0123456789abcdef", "adv_name": "Test Contact"}
await command_handler.send_msg(dst, "test message")
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x00\x00")
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
# Command tests
async def test_send_login(command_handler, mock_connection):
await command_handler.send_login(VALID_PUBKEY_HEX, "password")
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x1a")
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
assert b"password" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
async def test_send_msg(command_handler, mock_connection):
await command_handler.send_msg("0123456789abcdef", "hello")
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x00\x00")
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
assert b"hello" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
async def test_send_cmd(command_handler, mock_connection):
await command_handler.send_cmd("0123456789abcdef", "test_cmd")
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x02\x01\x00")
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
assert b"test_cmd" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
# Device settings tests
async def test_set_name(command_handler, mock_connection):
await command_handler.set_name("Test Device")
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x08")
assert b"Test Device" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
async def test_set_coords(command_handler, mock_connection):
await command_handler.set_coords(37.7749, -122.4194)
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x0e")
# Could add more detailed assertions for the byte encoding
2025-04-13 22:55:31 -07:00
async def test_send_appstart(command_handler, mock_connection):
await command_handler.send_appstart()
assert mock_connection.send.call_args[0][0].startswith(b"\x01\x03")
assert b"mccli" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
async def test_send_device_query(command_handler, mock_connection):
await command_handler.send_device_query()
assert mock_connection.send.call_args[0][0].startswith(b"\x16\x03")
2025-04-13 22:55:31 -07:00
async def test_send_advert(command_handler, mock_connection):
# Test without flood
await command_handler.send_advert(flood=False)
assert mock_connection.send.call_args[0][0] == b"\x07"
2025-04-13 22:55:31 -07:00
# Test with flood
mock_connection.reset_mock()
await command_handler.send_advert(flood=True)
assert mock_connection.send.call_args[0][0] == b"\x07\x01"
2025-04-13 22:55:31 -07:00
async def test_reboot(command_handler, mock_connection):
await command_handler.reboot()
assert mock_connection.send.call_args[0][0].startswith(b"\x13reboot")
2025-04-13 22:55:31 -07:00
async def test_get_bat(command_handler, mock_connection):
await command_handler.get_bat()
assert mock_connection.send.call_args[0][0].startswith(b"\x14")
2025-04-13 22:55:31 -07:00
async def test_get_time(command_handler, mock_connection):
await command_handler.get_time()
assert mock_connection.send.call_args[0][0].startswith(b"\x05")
2025-04-13 22:55:31 -07:00
async def test_set_time(command_handler, mock_connection):
timestamp = 1620000000 # Example timestamp
await command_handler.set_time(timestamp)
assert mock_connection.send.call_args[0][0].startswith(b"\x06")
2025-04-13 22:55:31 -07:00
async def test_set_tx_power(command_handler, mock_connection):
await command_handler.set_tx_power(20)
assert mock_connection.send.call_args[0][0].startswith(b"\x0c")
2025-04-13 22:55:31 -07:00
async def test_get_contacts(command_handler, mock_connection):
await command_handler.get_contacts()
assert mock_connection.send.call_args[0][0].startswith(b"\x04")
2025-04-13 22:55:31 -07:00
async def test_reset_path(command_handler, mock_connection):
command_handler._get_contact_by_prefix = lambda prefix: None
await command_handler.reset_path(VALID_PUBKEY_HEX)
assert mock_connection.send.call_args[0][0].startswith(b"\x0d")
2025-04-13 22:55:31 -07:00
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
async def test_share_contact(command_handler, mock_connection):
await command_handler.share_contact(VALID_PUBKEY_HEX)
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x10")
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
async def test_export_contact(command_handler, mock_connection):
# Test exporting all contacts
await command_handler.export_contact()
assert mock_connection.send.call_args[0][0] == b"\x11"
2025-04-13 22:55:31 -07:00
# Test exporting specific contact
mock_connection.reset_mock()
await command_handler.export_contact(VALID_PUBKEY_HEX)
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x11")
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
async def test_remove_contact(command_handler, mock_connection):
await command_handler.remove_contact(VALID_PUBKEY_HEX)
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x0f")
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
async def test_get_msg(command_handler, mock_connection):
await command_handler.get_msg()
assert mock_connection.send.call_args[0][0].startswith(b"\x0a")
2025-04-13 22:55:31 -07:00
# Test with custom timeout
mock_connection.reset_mock()
await command_handler.get_msg(timeout=5.0)
assert mock_connection.send.call_args[0][0].startswith(b"\x0a")
2025-04-13 22:55:31 -07:00
async def test_send_logout(command_handler, mock_connection):
await command_handler.send_logout(VALID_PUBKEY_HEX)
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x1d")
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
async def test_send_statusreq(command_handler, mock_connection):
await command_handler.send_statusreq(VALID_PUBKEY_HEX)
2025-04-13 22:55:31 -07:00
assert mock_connection.send.call_args[0][0].startswith(b"\x1b")
assert b"\x01\x23\x45\x67\x89\xab" in mock_connection.send.call_args[0][0]
2025-04-13 22:55:31 -07:00
async def test_send_trace(command_handler, mock_connection):
# Test with minimal parameters
await command_handler.send_trace()
first_call = mock_connection.send.call_args[0][0]
assert first_call.startswith(b"\x24") # 36 in decimal = 0x24 in hex
# Test with all parameters (flags=1 means path_hash_len=2, so 4 hex chars each)
2025-04-13 22:55:31 -07:00
mock_connection.reset_mock()
await command_handler.send_trace(
auth_code=12345, tag=67890, flags=1, path="0123,2345,4567"
2025-04-13 22:55:31 -07:00
)
second_call = mock_connection.send.call_args[0][0]
assert second_call.startswith(b"\x24")
async def test_send_with_multiple_expected_events_returns_first_completed(
command_handler, mock_connection, mock_dispatcher
):
error_payload = {"reason": "command_failed"}
setup_event_response(mock_dispatcher, EventType.ERROR, error_payload)
result = await command_handler.send(
b"test_command", [EventType.OK, EventType.ERROR]
)
mock_connection.send.assert_called_once_with(b"test_command")
assert result.type == EventType.ERROR
assert result.payload == error_payload
# Channel command tests
async def test_get_channel(command_handler, mock_connection):
await command_handler.get_channel(3)
assert mock_connection.send.call_args[0][0] == b"\x1f\x03"
async def test_set_channel(command_handler, mock_connection):
channel_secret = bytes(range(16)) # 16 bytes: 0x00, 0x01, ..., 0x0f
await command_handler.set_channel(5, "MyChannel", channel_secret)
expected_data = b"\x20\x05" # CMD_SET_CHANNEL + channel_idx=5
expected_data += b"MyChannel" + b"\x00" * (
32 - len("MyChannel")
) # 32-byte padded name
expected_data += channel_secret # 16-byte secret
assert mock_connection.send.call_args[0][0] == expected_data
async def test_set_channel_invalid_secret_length(command_handler):
with pytest.raises(ValueError, match="Channel secret must be exactly 16 bytes"):
await command_handler.set_channel(1, "Test", b"tooshort")
async def test_send_chan_msg_with_str_timestamp(command_handler, mock_connection):
ts = 1620000000
await command_handler.send_chan_msg(3, "world", timestamp=ts)
data = mock_connection.send.call_args[0][0]
assert data.startswith(b"\x03\x00\x03")
assert b"world" in data
assert data[3:7] == ts.to_bytes(4, "little")
async def test_send_chan_msg_with_bytes_timestamp(command_handler, mock_connection):
ts = 1620000000
await command_handler.send_chan_msg(3, "world", timestamp=ts.to_bytes(4, "little"))
data = mock_connection.send.call_args[0][0]
assert data.startswith(b"\x03\x00\x03")
assert b"world" in data
assert data[3:7] == ts.to_bytes(4, "little")
async def test_send_chan_msg_with_invalid_timestamp(command_handler, mock_connection):
result = await command_handler.send_chan_msg(3, "world", timestamp=b"00")
assert result.type == EventType.ERROR
assert result.payload["reason"] == "invalid_timestamp_format"