mirror of
https://github.com/craigerl/aprsd.git
synced 2026-03-15 04:19:47 -04:00
The client registry defined a protocol that all drivers had to implement. This patch ensures that all methods are consistent in the protocol definition.
459 lines
17 KiB
Python
459 lines
17 KiB
Python
import datetime
|
|
import socket
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
import aprslib
|
|
|
|
from aprsd import exception
|
|
from aprsd.client.drivers.registry import ClientDriver
|
|
from aprsd.client.drivers.tcpkiss import TCPKISSDriver
|
|
from aprsd.packets import core
|
|
|
|
|
|
class TestTCPKISSDriver(unittest.TestCase):
|
|
"""Unit tests for the TCPKISSDriver class."""
|
|
|
|
def setUp(self):
|
|
# Mock configuration
|
|
self.conf_patcher = mock.patch('aprsd.client.drivers.tcpkiss.CONF')
|
|
self.mock_conf = self.conf_patcher.start()
|
|
|
|
# Configure KISS settings
|
|
self.mock_conf.kiss_tcp.enabled = True
|
|
self.mock_conf.kiss_tcp.host = '127.0.0.1'
|
|
self.mock_conf.kiss_tcp.port = 8001
|
|
self.mock_conf.kiss_tcp.path = ['WIDE1-1', 'WIDE2-1']
|
|
|
|
# Mock socket
|
|
self.socket_patcher = mock.patch('aprsd.client.drivers.tcpkiss.socket')
|
|
self.mock_socket_module = self.socket_patcher.start()
|
|
self.mock_socket = mock.MagicMock()
|
|
self.mock_socket_module.socket.return_value = self.mock_socket
|
|
|
|
# Mock select
|
|
self.select_patcher = mock.patch('aprsd.client.drivers.tcpkiss.select')
|
|
self.mock_select = self.select_patcher.start()
|
|
|
|
# Create an instance of the driver
|
|
self.driver = TCPKISSDriver()
|
|
|
|
def tearDown(self):
|
|
self.conf_patcher.stop()
|
|
self.socket_patcher.stop()
|
|
self.select_patcher.stop()
|
|
|
|
def test_implements_client_driver_protocol(self):
|
|
"""Test that TCPKISSDriver implements the ClientDriver Protocol."""
|
|
# Verify the instance is recognized as implementing the Protocol
|
|
self.assertIsInstance(self.driver, ClientDriver)
|
|
|
|
# Verify all required methods are present with correct signatures
|
|
required_methods = [
|
|
'is_enabled',
|
|
'is_configured',
|
|
'is_alive',
|
|
'close',
|
|
'send',
|
|
'setup_connection',
|
|
'set_filter',
|
|
'login_success',
|
|
'login_failure',
|
|
'consumer',
|
|
'decode_packet',
|
|
'stats',
|
|
]
|
|
|
|
for method_name in required_methods:
|
|
self.assertTrue(
|
|
hasattr(self.driver, method_name),
|
|
f'Missing required method: {method_name}',
|
|
)
|
|
|
|
def test_init(self):
|
|
"""Test initialization sets default values."""
|
|
self.assertFalse(self.driver._connected)
|
|
self.assertIsInstance(self.driver.keepalive, datetime.datetime)
|
|
self.assertFalse(self.driver._running)
|
|
|
|
def test_transport_property(self):
|
|
"""Test transport method returns correct value."""
|
|
self.assertEqual(self.driver.transport(), 'tcpkiss')
|
|
|
|
def test_is_enabled_true(self):
|
|
"""Test is_enabled returns True when KISS TCP is enabled."""
|
|
self.mock_conf.kiss_tcp.enabled = True
|
|
self.assertTrue(TCPKISSDriver.is_enabled())
|
|
|
|
def test_is_enabled_false(self):
|
|
"""Test is_enabled returns False when KISS TCP is disabled."""
|
|
self.mock_conf.kiss_tcp.enabled = False
|
|
self.assertFalse(TCPKISSDriver.is_enabled())
|
|
|
|
def test_is_configured_true(self):
|
|
"""Test is_configured returns True when properly configured."""
|
|
with mock.patch.object(TCPKISSDriver, 'is_enabled', return_value=True):
|
|
self.mock_conf.kiss_tcp.host = '127.0.0.1'
|
|
self.assertTrue(TCPKISSDriver.is_configured())
|
|
|
|
def test_is_configured_false_no_host(self):
|
|
"""Test is_configured returns False when host not set."""
|
|
with mock.patch.object(TCPKISSDriver, 'is_enabled', return_value=True):
|
|
self.mock_conf.kiss_tcp.host = None
|
|
with self.assertRaises(exception.MissingConfigOptionException):
|
|
TCPKISSDriver.is_configured()
|
|
|
|
def test_is_configured_false_not_enabled(self):
|
|
"""Test is_configured returns False when not enabled."""
|
|
with mock.patch.object(TCPKISSDriver, 'is_enabled', return_value=False):
|
|
self.assertFalse(TCPKISSDriver.is_configured())
|
|
|
|
def test_is_alive(self):
|
|
"""Test is_alive property returns connection state."""
|
|
self.driver._connected = True
|
|
self.assertTrue(self.driver.is_alive)
|
|
|
|
self.driver._connected = False
|
|
self.assertFalse(self.driver.is_alive)
|
|
|
|
def test_close(self):
|
|
"""Test close method calls stop."""
|
|
with mock.patch.object(self.driver, 'socket') as mock_socket:
|
|
self.driver.close()
|
|
mock_socket.close.assert_called_once()
|
|
self.assertFalse(self.driver._connected)
|
|
|
|
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
|
def test_setup_connection_success(self, mock_log):
|
|
"""Test setup_connection successfully connects."""
|
|
# Mock the connect method to succeed
|
|
is_en = self.driver.is_enabled
|
|
is_con = self.driver.is_configured
|
|
self.driver.is_enabled = mock.MagicMock(return_value=True)
|
|
self.driver.is_configured = mock.MagicMock(return_value=True)
|
|
with mock.patch.object(
|
|
self.driver, 'connect', return_value=True
|
|
) as mock_connect:
|
|
self.driver.setup_connection()
|
|
mock_connect.assert_called_once()
|
|
mock_log.info.assert_called_with('KISS TCP Connection to 127.0.0.1:8001')
|
|
|
|
self.driver.is_enabled = is_en
|
|
self.driver.is_configured = is_con
|
|
|
|
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
|
def test_setup_connection_failure(self, mock_log):
|
|
"""Test setup_connection handles connection failure."""
|
|
# Mock the connect method to fail
|
|
with mock.patch.object(
|
|
self.driver, 'connect', return_value=False
|
|
) as mock_connect:
|
|
self.driver.setup_connection()
|
|
mock_connect.assert_called_once()
|
|
mock_log.error.assert_called_with('Failed to connect to KISS interface')
|
|
|
|
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
|
def test_setup_connection_exception(self, mock_log):
|
|
"""Test setup_connection handles exceptions."""
|
|
# Mock the connect method to raise an exception
|
|
with mock.patch.object(
|
|
self.driver, 'connect', side_effect=Exception('Test error')
|
|
) as mock_connect:
|
|
self.driver.setup_connection()
|
|
mock_connect.assert_called_once()
|
|
mock_log.error.assert_any_call('Failed to initialize KISS interface')
|
|
mock_log.exception.assert_called_once()
|
|
self.assertFalse(self.driver._connected)
|
|
|
|
def test_set_filter(self):
|
|
"""Test set_filter does nothing for KISS."""
|
|
# Just ensure it doesn't fail
|
|
self.driver.set_filter('test/filter')
|
|
|
|
def test_login_success_when_connected(self):
|
|
"""Test login_success returns True when connected."""
|
|
self.driver._connected = True
|
|
self.assertTrue(self.driver.login_success())
|
|
|
|
def test_login_success_when_not_connected(self):
|
|
"""Test login_success returns False when not connected."""
|
|
self.driver._connected = False
|
|
self.assertFalse(self.driver.login_success())
|
|
|
|
def test_login_failure(self):
|
|
"""Test login_failure returns success message."""
|
|
self.assertEqual(self.driver.login_failure(), 'Login successful')
|
|
|
|
@mock.patch('aprsd.client.drivers.tcpkiss.ax25frame.Frame.ui')
|
|
def test_send_packet(self, mock_frame_ui):
|
|
"""Test sending an APRS packet."""
|
|
# Create a mock frame
|
|
mock_frame = mock.MagicMock()
|
|
mock_frame_bytes = b'mock_frame_data'
|
|
mock_frame.__bytes__ = mock.MagicMock(return_value=mock_frame_bytes)
|
|
mock_frame_ui.return_value = mock_frame
|
|
|
|
# Set up the driver
|
|
self.driver.socket = self.mock_socket
|
|
self.driver.path = ['WIDE1-1', 'WIDE2-1']
|
|
|
|
# Create a mock packet
|
|
mock_packet = mock.MagicMock(spec=core.Packet)
|
|
mock_bytes = b'Test packet data'
|
|
mock_packet.__bytes__ = mock.MagicMock(return_value=mock_bytes)
|
|
# Add path attribute to the mock packet
|
|
mock_packet.path = None
|
|
|
|
# Send the packet
|
|
self.driver.send(mock_packet)
|
|
|
|
# Check that frame was created correctly
|
|
mock_frame_ui.assert_called_once_with(
|
|
destination='APZ100',
|
|
source=mock_packet.from_call,
|
|
path=self.driver.path,
|
|
info=mock_packet.payload.encode('US-ASCII'),
|
|
)
|
|
|
|
# Check that socket send was called
|
|
self.mock_socket.send.assert_called_once()
|
|
|
|
# Verify packet counters updated
|
|
self.assertEqual(self.driver.packets_sent, 1)
|
|
self.assertIsNotNone(self.driver.last_packet_sent)
|
|
|
|
def test_send_with_no_socket(self):
|
|
"""Test send raises exception when socket not initialized."""
|
|
self.driver.socket = None
|
|
mock_packet = mock.MagicMock(spec=core.Packet)
|
|
|
|
with self.assertRaises(Exception) as context:
|
|
self.driver.send(mock_packet)
|
|
self.assertIn('KISS interface not initialized', str(context.exception))
|
|
|
|
def test_stats(self):
|
|
"""Test stats method returns correct data."""
|
|
# Set up test data
|
|
self.driver._connected = True
|
|
self.driver.path = ['WIDE1-1', 'WIDE2-1']
|
|
self.driver.packets_sent = 5
|
|
self.driver.packets_received = 3
|
|
self.driver.last_packet_sent = datetime.datetime.now()
|
|
self.driver.last_packet_received = datetime.datetime.now()
|
|
|
|
# Get stats
|
|
stats = self.driver.stats()
|
|
|
|
# Check stats contains expected keys
|
|
expected_keys = [
|
|
'client',
|
|
'transport',
|
|
'connected',
|
|
'path',
|
|
'packets_sent',
|
|
'packets_received',
|
|
'last_packet_sent',
|
|
'last_packet_received',
|
|
'connection_keepalive',
|
|
'host',
|
|
'port',
|
|
]
|
|
for key in expected_keys:
|
|
self.assertIn(key, stats)
|
|
|
|
# Check some specific values
|
|
self.assertEqual(stats['client'], 'TCPKISSDriver')
|
|
self.assertEqual(stats['transport'], 'tcpkiss')
|
|
self.assertEqual(stats['connected'], True)
|
|
self.assertEqual(stats['packets_sent'], 5)
|
|
self.assertEqual(stats['packets_received'], 3)
|
|
|
|
def test_stats_serializable(self):
|
|
"""Test stats with serializable=True converts datetime to ISO format."""
|
|
self.driver.keepalive = datetime.datetime.now()
|
|
|
|
stats = self.driver.stats(serializable=True)
|
|
|
|
# Check keepalive is a string in ISO format
|
|
self.assertIsInstance(stats['connection_keepalive'], str)
|
|
# Try parsing it to verify it's a valid ISO format
|
|
try:
|
|
datetime.datetime.fromisoformat(stats['connection_keepalive'])
|
|
except ValueError:
|
|
self.fail('keepalive is not in valid ISO format')
|
|
|
|
def test_connect_success(self):
|
|
"""Test successful connection."""
|
|
result = self.driver.connect()
|
|
|
|
self.assertTrue(result)
|
|
self.assertTrue(self.driver._connected)
|
|
self.mock_socket.connect.assert_called_once_with(
|
|
(self.mock_conf.kiss_tcp.host, self.mock_conf.kiss_tcp.port)
|
|
)
|
|
self.mock_socket.settimeout.assert_any_call(5.0)
|
|
self.mock_socket.settimeout.assert_any_call(0.1)
|
|
|
|
def test_connect_failure_socket_error(self):
|
|
"""Test connection failure due to socket error."""
|
|
self.mock_socket.connect.side_effect = socket.error('Test socket error')
|
|
|
|
result = self.driver.connect()
|
|
|
|
self.assertFalse(result)
|
|
self.assertFalse(self.driver._connected)
|
|
|
|
def test_connect_failure_timeout(self):
|
|
"""Test connection failure due to timeout."""
|
|
self.mock_socket.connect.side_effect = socket.timeout('Test timeout')
|
|
|
|
result = self.driver.connect()
|
|
|
|
self.assertFalse(result)
|
|
self.assertFalse(self.driver._connected)
|
|
|
|
def test_fix_raw_frame(self):
|
|
"""Test fix_raw_frame removes KISS markers and handles FEND."""
|
|
# Create a test frame with KISS markers
|
|
with mock.patch(
|
|
'aprsd.client.drivers.kiss_common.KISSDriver._handle_fend',
|
|
return_value=b'fixed_frame',
|
|
) as mock_handle_fend:
|
|
raw_frame = b'\xc0\x00some_frame_data\xc0' # \xc0 is FEND
|
|
|
|
result = self.driver.fix_raw_frame(raw_frame)
|
|
|
|
mock_handle_fend.assert_called_once_with(b'some_frame_data')
|
|
self.assertEqual(result, b'fixed_frame')
|
|
|
|
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
|
def test_decode_packet_success(self, mock_log):
|
|
"""Test successful packet decoding."""
|
|
mock_frame = 'test frame data'
|
|
mock_aprs_data = {'from': 'TEST-1', 'to': 'APRS'}
|
|
mock_packet = mock.MagicMock(spec=core.Packet)
|
|
|
|
with mock.patch(
|
|
'aprsd.client.drivers.tcpkiss.aprslib.parse', return_value=mock_aprs_data
|
|
) as mock_parse:
|
|
with mock.patch(
|
|
'aprsd.client.drivers.tcpkiss.core.factory', return_value=mock_packet
|
|
) as mock_factory:
|
|
result = self.driver.decode_packet(mock_frame)
|
|
|
|
mock_parse.assert_called_once_with(str(mock_frame))
|
|
mock_factory.assert_called_once_with(mock_aprs_data)
|
|
self.assertEqual(result, mock_packet)
|
|
|
|
@mock.patch('aprsd.client.drivers.kiss_common.LOG')
|
|
def test_decode_packet_no_frame(self, mock_log):
|
|
"""Test decode_packet with no frame returns None."""
|
|
result = self.driver.decode_packet()
|
|
|
|
self.assertIsNone(result)
|
|
mock_log.warning.assert_called_once()
|
|
|
|
@mock.patch('aprsd.client.drivers.kiss_common.LOG')
|
|
def test_decode_packet_exception(self, mock_log):
|
|
"""Test decode_packet handles exceptions."""
|
|
mock_frame = 'invalid frame'
|
|
|
|
with mock.patch(
|
|
'aprsd.client.drivers.kiss_common.aprslib.parse',
|
|
side_effect=Exception('Test error'),
|
|
) as mock_parse:
|
|
result = self.driver.decode_packet(mock_frame)
|
|
|
|
mock_parse.assert_called_once()
|
|
self.assertIsNone(result)
|
|
mock_log.error.assert_called_once()
|
|
|
|
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
|
def test_consumer_with_frame(self, mock_log):
|
|
"""Test consumer processes frames and calls callback."""
|
|
mock_callback = mock.MagicMock()
|
|
mock_frame = mock.MagicMock()
|
|
|
|
# Configure driver for test
|
|
self.driver._connected = True
|
|
self.driver._running = True
|
|
|
|
# Set up read_frame to return one frame then stop
|
|
def side_effect():
|
|
self.driver._running = False
|
|
return mock_frame
|
|
|
|
with mock.patch.object(
|
|
self.driver, 'read_frame', side_effect=side_effect
|
|
) as mock_read_frame:
|
|
self.driver.consumer(mock_callback)
|
|
|
|
mock_read_frame.assert_called_once()
|
|
mock_callback.assert_called_once_with(mock_frame)
|
|
|
|
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
|
def test_read_frame_success(self, mock_log):
|
|
"""Test read_frame successfully reads a frame."""
|
|
# Set up driver
|
|
self.driver.socket = self.mock_socket
|
|
self.driver._connected = True
|
|
|
|
# Mock socket recv to return data
|
|
raw_data = b'\xc0\x00test_frame\xc0'
|
|
self.mock_socket.recv.return_value = raw_data
|
|
|
|
# Mock select to indicate socket is readable
|
|
self.mock_select.select.return_value = ([self.mock_socket], [], [])
|
|
|
|
# Mock fix_raw_frame and Frame.from_bytes
|
|
mock_fixed_frame = b'fixed_frame'
|
|
mock_ax25_frame = mock.MagicMock()
|
|
|
|
with mock.patch.object(
|
|
self.driver, 'fix_raw_frame', return_value=mock_fixed_frame
|
|
) as mock_fix:
|
|
with mock.patch(
|
|
'aprsd.client.drivers.tcpkiss.ax25frame.Frame.from_bytes',
|
|
return_value=mock_ax25_frame,
|
|
) as mock_from_bytes:
|
|
result = self.driver.read_frame()
|
|
|
|
self.mock_socket.setblocking.assert_called_once_with(0)
|
|
self.mock_select.select.assert_called_once()
|
|
self.mock_socket.recv.assert_called_once()
|
|
mock_fix.assert_called_once_with(raw_data)
|
|
mock_from_bytes.assert_called_once_with(mock_fixed_frame)
|
|
self.assertEqual(result, mock_ax25_frame)
|
|
|
|
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
|
def test_read_frame_select_timeout(self, mock_log):
|
|
"""Test read_frame handles select timeout."""
|
|
# Set up driver
|
|
self.driver.socket = self.mock_socket
|
|
self.driver._running = True
|
|
|
|
# Mock select to indicate no readable sockets
|
|
self.mock_select.select.return_value = ([], [], [])
|
|
|
|
result = self.driver.read_frame()
|
|
|
|
self.assertIsNone(result)
|
|
|
|
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
|
def test_read_frame_socket_error(self, mock_log):
|
|
"""Test read_frame handles socket error."""
|
|
# Set up driver
|
|
self.driver.socket = self.mock_socket
|
|
self.driver._connected = True
|
|
|
|
# Mock setblocking to raise OSError
|
|
self.mock_socket.setblocking.side_effect = OSError('Test error')
|
|
|
|
with self.assertRaises(aprslib.ConnectionDrop):
|
|
self.driver.read_frame()
|
|
mock_log.error.assert_called_once()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|