test: repair stale assertions in validation/waterfall/meshtastic/routes

Auth fixture, /listening->/receiver waterfall rename, numeric validator
returns, and float timestamp — all matching current code behaviour.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
James Smith
2026-06-12 14:56:02 +01:00
parent 73e05ed31b
commit 60480ccf37
4 changed files with 355 additions and 375 deletions
+109 -111
View File
@@ -18,12 +18,14 @@ import pytest
# Utility Module Tests
# =============================================================================
class TestMeshtasticAvailability:
"""Tests for SDK availability checks."""
def test_is_meshtastic_available_returns_bool(self):
"""is_meshtastic_available should return a boolean."""
from utils.meshtastic import is_meshtastic_available
result = is_meshtastic_available()
assert isinstance(result, bool)
@@ -36,10 +38,10 @@ class TestMeshtasticMessage:
from utils.meshtastic import MeshtasticMessage
msg = MeshtasticMessage(
from_id='!a1b2c3d4',
to_id='^all',
message='Hello mesh!',
portnum='TEXT_MESSAGE_APP',
from_id="!a1b2c3d4",
to_id="^all",
message="Hello mesh!",
portnum="TEXT_MESSAGE_APP",
channel=0,
rssi=-95,
snr=-3.5,
@@ -49,26 +51,28 @@ class TestMeshtasticMessage:
d = msg.to_dict()
assert d['type'] == 'meshtastic'
assert d['from'] == '!a1b2c3d4'
assert d['to'] == '^all'
assert d['message'] == 'Hello mesh!'
assert d['portnum'] == 'TEXT_MESSAGE_APP'
assert d['channel'] == 0
assert d['rssi'] == -95
assert d['snr'] == -3.5
assert d['hop_limit'] == 3
assert '2026-01-27' in d['timestamp']
assert d["type"] == "meshtastic"
assert d["from"] == "!a1b2c3d4"
assert d["to"] == "^all"
assert d["message"] == "Hello mesh!"
assert d["portnum"] == "TEXT_MESSAGE_APP"
assert d["channel"] == 0
assert d["rssi"] == -95
assert d["snr"] == -3.5
assert d["hop_limit"] == 3
assert isinstance(d["timestamp"], float)
# 2026-01-27 12:00:00 UTC as Unix epoch
assert d["timestamp"] == pytest.approx(1769515200.0)
def test_message_with_none_values(self):
"""MeshtasticMessage should handle None values."""
from utils.meshtastic import MeshtasticMessage
msg = MeshtasticMessage(
from_id='!00000001',
to_id='!00000002',
from_id="!00000001",
to_id="!00000002",
message=None,
portnum='POSITION_APP',
portnum="POSITION_APP",
channel=1,
rssi=None,
snr=None,
@@ -78,9 +82,9 @@ class TestMeshtasticMessage:
d = msg.to_dict()
assert d['message'] is None
assert d['rssi'] is None
assert d['snr'] is None
assert d["message"] is None
assert d["rssi"] is None
assert d["snr"] is None
class TestChannelConfig:
@@ -92,50 +96,50 @@ class TestChannelConfig:
config = ChannelConfig(
index=0,
name='Primary',
psk=b'\x01\x02\x03\x04' * 8, # 32-byte key
name="Primary",
psk=b"\x01\x02\x03\x04" * 8, # 32-byte key
role=1, # PRIMARY
)
d = config.to_dict()
assert 'psk' not in d # Raw PSK should not be in dict
assert d['index'] == 0
assert d['name'] == 'Primary'
assert d['role'] == 'PRIMARY'
assert d['encrypted'] is True
assert d['key_type'] == 'AES-256'
assert "psk" not in d # Raw PSK should not be in dict
assert d["index"] == 0
assert d["name"] == "Primary"
assert d["role"] == "PRIMARY"
assert d["encrypted"] is True
assert d["key_type"] == "AES-256"
def test_channel_default_key_detection(self):
"""ChannelConfig should detect default key."""
from utils.meshtastic import ChannelConfig
# Default key is single byte 0x01
config = ChannelConfig(index=0, name='Test', psk=b'\x01', role=1)
config = ChannelConfig(index=0, name="Test", psk=b"\x01", role=1)
d = config.to_dict()
assert d['is_default_key'] is True
assert d['key_type'] == 'default'
assert d["is_default_key"] is True
assert d["key_type"] == "default"
def test_channel_aes128_detection(self):
"""ChannelConfig should detect AES-128 key."""
from utils.meshtastic import ChannelConfig
config = ChannelConfig(index=0, name='Test', psk=b'0' * 16, role=1)
config = ChannelConfig(index=0, name="Test", psk=b"0" * 16, role=1)
d = config.to_dict()
assert d['key_type'] == 'AES-128'
assert d['encrypted'] is True
assert d["key_type"] == "AES-128"
assert d["encrypted"] is True
def test_channel_no_encryption(self):
"""ChannelConfig should detect no encryption."""
from utils.meshtastic import ChannelConfig
config = ChannelConfig(index=0, name='Test', psk=b'', role=1)
config = ChannelConfig(index=0, name="Test", psk=b"", role=1)
d = config.to_dict()
assert d['key_type'] == 'none'
assert d['encrypted'] is False
assert d["key_type"] == "none"
assert d["encrypted"] is False
class TestPSKParsing:
@@ -146,29 +150,29 @@ class TestPSKParsing:
from utils.meshtastic import MeshtasticClient
client = MeshtasticClient()
result = client._parse_psk('none')
result = client._parse_psk("none")
assert result == b''
assert result == b""
def test_parse_psk_default(self):
"""Should parse 'default' as single byte."""
from utils.meshtastic import MeshtasticClient
client = MeshtasticClient()
result = client._parse_psk('default')
result = client._parse_psk("default")
assert result == b'\x01'
assert result == b"\x01"
def test_parse_psk_random(self):
"""Should generate 32 random bytes for 'random'."""
from utils.meshtastic import MeshtasticClient
client = MeshtasticClient()
result = client._parse_psk('random')
result = client._parse_psk("random")
assert len(result) == 32
# Verify it's actually random (two calls should differ)
result2 = client._parse_psk('random')
result2 = client._parse_psk("random")
assert result != result2
def test_parse_psk_base64(self):
@@ -179,8 +183,8 @@ class TestPSKParsing:
client = MeshtasticClient()
# 32-byte key encoded as base64
key = b'A' * 32
encoded = 'base64:' + base64.b64encode(key).decode()
key = b"A" * 32
encoded = "base64:" + base64.b64encode(key).decode()
result = client._parse_psk(encoded)
@@ -192,9 +196,9 @@ class TestPSKParsing:
client = MeshtasticClient()
# 16-byte key as hex
result = client._parse_psk('0x' + '41' * 16)
result = client._parse_psk("0x" + "41" * 16)
assert result == b'A' * 16
assert result == b"A" * 16
def test_parse_psk_simple_passphrase(self):
"""Should hash simple passphrase to 32-byte key."""
@@ -203,9 +207,9 @@ class TestPSKParsing:
from utils.meshtastic import MeshtasticClient
client = MeshtasticClient()
result = client._parse_psk('simple:MySecretPassword')
result = client._parse_psk("simple:MySecretPassword")
expected = hashlib.sha256(b'MySecretPassword').digest()
expected = hashlib.sha256(b"MySecretPassword").digest()
assert result == expected
assert len(result) == 32
@@ -215,8 +219,8 @@ class TestPSKParsing:
client = MeshtasticClient()
assert client._parse_psk('base64:!!!invalid!!!') is None
assert client._parse_psk('0xZZZZ') is None
assert client._parse_psk("base64:!!!invalid!!!") is None
assert client._parse_psk("0xZZZZ") is None
def test_parse_psk_raw_base64(self):
"""Should accept raw base64 without prefix."""
@@ -225,7 +229,7 @@ class TestPSKParsing:
from utils.meshtastic import MeshtasticClient
client = MeshtasticClient()
key = b'B' * 16
key = b"B" * 16
encoded = base64.b64encode(key).decode()
result = client._parse_psk(encoded)
@@ -242,7 +246,7 @@ class TestNodeIdFormatting:
result = MeshtasticClient._format_node_id(0xDEADBEEF)
assert result == '!deadbeef'
assert result == "!deadbeef"
def test_format_broadcast(self):
"""Should format broadcast address."""
@@ -250,13 +254,14 @@ class TestNodeIdFormatting:
result = MeshtasticClient._format_node_id(0xFFFFFFFF)
assert result == '^all'
assert result == "^all"
# =============================================================================
# Route Tests (Mocked)
# =============================================================================
class TestMeshtasticRoutes:
"""Tests for Flask route endpoints."""
@@ -268,7 +273,7 @@ class TestMeshtasticRoutes:
from routes.meshtastic import meshtastic_bp
app = Flask(__name__)
app.config['TESTING'] = True
app.config["TESTING"] = True
app.register_blueprint(meshtastic_bp)
return app
@@ -280,144 +285,137 @@ class TestMeshtasticRoutes:
def test_status_sdk_not_installed(self, client):
"""GET /meshtastic/status should report SDK unavailable."""
with patch('routes.meshtastic.is_meshtastic_available', return_value=False):
response = client.get('/meshtastic/status')
with patch("routes.meshtastic.is_meshtastic_available", return_value=False):
response = client.get("/meshtastic/status")
data = json.loads(response.data)
assert response.status_code == 200
assert data['available'] is False
assert 'not installed' in data['error']
assert data["available"] is False
assert "not installed" in data["error"]
def test_status_not_connected(self, client):
"""GET /meshtastic/status should report not running when disconnected."""
with patch('routes.meshtastic.is_meshtastic_available', return_value=True):
with patch('routes.meshtastic.get_meshtastic_client', return_value=None):
response = client.get('/meshtastic/status')
with patch("routes.meshtastic.is_meshtastic_available", return_value=True):
with patch("routes.meshtastic.get_meshtastic_client", return_value=None):
response = client.get("/meshtastic/status")
data = json.loads(response.data)
assert response.status_code == 200
assert data['available'] is True
assert data['running'] is False
assert data["available"] is True
assert data["running"] is False
def test_start_sdk_not_installed(self, client):
"""POST /meshtastic/start should fail if SDK not installed."""
with patch('routes.meshtastic.is_meshtastic_available', return_value=False):
response = client.post('/meshtastic/start')
with patch("routes.meshtastic.is_meshtastic_available", return_value=False):
response = client.post("/meshtastic/start")
data = json.loads(response.data)
assert response.status_code == 400
assert data['status'] == 'error'
assert data["status"] == "error"
def test_stop_always_succeeds(self, client):
"""POST /meshtastic/stop should always succeed."""
with patch('routes.meshtastic.stop_meshtastic'):
response = client.post('/meshtastic/stop')
with patch("routes.meshtastic.stop_meshtastic"):
response = client.post("/meshtastic/stop")
data = json.loads(response.data)
assert response.status_code == 200
assert data['status'] == 'stopped'
assert data["status"] == "stopped"
def test_channels_not_connected(self, client):
"""GET /meshtastic/channels should fail if not connected."""
with patch('routes.meshtastic.get_meshtastic_client', return_value=None):
response = client.get('/meshtastic/channels')
with patch("routes.meshtastic.get_meshtastic_client", return_value=None):
response = client.get("/meshtastic/channels")
data = json.loads(response.data)
assert response.status_code == 400
assert 'Not connected' in data['message']
assert "Not connected" in data["message"]
def test_configure_channel_invalid_index(self, client):
"""POST /meshtastic/channels/<id> should reject invalid index."""
mock_client = Mock()
mock_client.is_running = True
with patch('routes.meshtastic.get_meshtastic_client', return_value=mock_client):
response = client.post(
'/meshtastic/channels/10',
json={'name': 'Test'},
content_type='application/json'
)
with patch("routes.meshtastic.get_meshtastic_client", return_value=mock_client):
response = client.post("/meshtastic/channels/10", json={"name": "Test"}, content_type="application/json")
data = json.loads(response.data)
assert response.status_code == 400
assert 'must be 0-7' in data['message']
assert "must be 0-7" in data["message"]
def test_configure_channel_no_params(self, client):
"""POST /meshtastic/channels/<id> should require name or psk."""
mock_client = Mock()
mock_client.is_running = True
with patch('routes.meshtastic.get_meshtastic_client', return_value=mock_client):
response = client.post(
'/meshtastic/channels/0',
json={},
content_type='application/json'
)
with patch("routes.meshtastic.get_meshtastic_client", return_value=mock_client):
response = client.post("/meshtastic/channels/0", json={}, content_type="application/json")
data = json.loads(response.data)
assert response.status_code == 400
assert 'Must provide' in data['message']
assert "Must provide" in data["message"]
def test_messages_empty(self, client):
"""GET /meshtastic/messages should return empty list initially."""
with patch('routes.meshtastic._recent_messages', []):
response = client.get('/meshtastic/messages')
with patch("routes.meshtastic._recent_messages", []):
response = client.get("/meshtastic/messages")
data = json.loads(response.data)
assert response.status_code == 200
assert data['status'] == 'ok'
assert data['messages'] == []
assert data['count'] == 0
assert data["status"] == "ok"
assert data["messages"] == []
assert data["count"] == 0
def test_messages_with_limit(self, client):
"""GET /meshtastic/messages should respect limit param."""
test_messages = [{'id': i} for i in range(10)]
test_messages = [{"id": i} for i in range(10)]
with patch('routes.meshtastic._recent_messages', test_messages):
response = client.get('/meshtastic/messages?limit=3')
with patch("routes.meshtastic._recent_messages", test_messages):
response = client.get("/meshtastic/messages?limit=3")
data = json.loads(response.data)
assert response.status_code == 200
assert len(data['messages']) == 3
assert len(data["messages"]) == 3
# Should return last 3 (most recent)
assert data['messages'][0]['id'] == 7
assert data["messages"][0]["id"] == 7
def test_messages_filter_by_channel(self, client):
"""GET /meshtastic/messages should filter by channel."""
test_messages = [
{'id': 1, 'channel': 0},
{'id': 2, 'channel': 1},
{'id': 3, 'channel': 0},
{"id": 1, "channel": 0},
{"id": 2, "channel": 1},
{"id": 3, "channel": 0},
]
with patch('routes.meshtastic._recent_messages', test_messages):
response = client.get('/meshtastic/messages?channel=0')
with patch("routes.meshtastic._recent_messages", test_messages):
response = client.get("/meshtastic/messages?channel=0")
data = json.loads(response.data)
assert response.status_code == 200
assert len(data['messages']) == 2
assert all(m['channel'] == 0 for m in data['messages'])
assert len(data["messages"]) == 2
assert all(m["channel"] == 0 for m in data["messages"])
def test_stream_endpoint_exists(self, client):
"""GET /meshtastic/stream should return SSE content type."""
response = client.get('/meshtastic/stream')
response = client.get("/meshtastic/stream")
assert response.content_type == 'text/event-stream'
assert response.content_type.startswith("text/event-stream")
def test_node_not_connected(self, client):
"""GET /meshtastic/node should fail if not connected."""
with patch('routes.meshtastic.get_meshtastic_client', return_value=None):
response = client.get('/meshtastic/node')
with patch("routes.meshtastic.get_meshtastic_client", return_value=None):
response = client.get("/meshtastic/node")
data = json.loads(response.data)
assert response.status_code == 400
assert 'Not connected' in data['message']
assert "Not connected" in data["message"]
# =============================================================================
# Integration Tests (Mocked SDK)
# =============================================================================
class TestMeshtasticClientMocked:
"""Tests for MeshtasticClient with mocked SDK."""
@@ -435,12 +433,12 @@ class TestMeshtasticClientMocked:
"""MeshtasticClient.connect should fail gracefully without SDK."""
from utils.meshtastic import MeshtasticClient
with patch('utils.meshtastic.HAS_MESHTASTIC', False):
with patch("utils.meshtastic.HAS_MESHTASTIC", False):
client = MeshtasticClient()
result = client.connect()
assert result is False
assert 'not installed' in client.error
assert "not installed" in client.error
def test_client_disconnect_idempotent(self):
"""MeshtasticClient.disconnect should be safe to call multiple times."""