mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-08-07 01:03:34 +02:00
Linting and code cleanup for an imitation of order
This commit is contained in:
@@ -13,4 +13,5 @@ def sample_channel_key():
|
||||
def sample_hashtag_key():
|
||||
"""A channel key derived from hashtag name '#test'."""
|
||||
import hashlib
|
||||
|
||||
return hashlib.sha256(b"#test").digest()[:16]
|
||||
|
||||
+80
-43
@@ -4,9 +4,10 @@ These tests verify the REST API behavior for critical operations.
|
||||
Uses FastAPI's TestClient for synchronous testing.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class TestHealthEndpoint:
|
||||
"""Test the health check endpoint."""
|
||||
@@ -20,6 +21,7 @@ class TestHealthEndpoint:
|
||||
mock_rm.port = "/dev/ttyUSB0"
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/health")
|
||||
@@ -38,6 +40,7 @@ class TestHealthEndpoint:
|
||||
mock_rm.port = None
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/health")
|
||||
@@ -60,11 +63,11 @@ class TestMessagesEndpoint:
|
||||
mock_rm.meshcore = None
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post(
|
||||
"/api/messages/direct",
|
||||
json={"destination": "abc123", "text": "Hello"}
|
||||
"/api/messages/direct", json={"destination": "abc123", "text": "Hello"}
|
||||
)
|
||||
|
||||
assert response.status_code == 503
|
||||
@@ -79,11 +82,12 @@ class TestMessagesEndpoint:
|
||||
mock_rm.meshcore = None
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post(
|
||||
"/api/messages/channel",
|
||||
json={"channel_key": "0123456789ABCDEF0123456789ABCDEF", "text": "Hello"}
|
||||
json={"channel_key": "0123456789ABCDEF0123456789ABCDEF", "text": "Hello"},
|
||||
)
|
||||
|
||||
assert response.status_code == 503
|
||||
@@ -95,18 +99,22 @@ class TestMessagesEndpoint:
|
||||
mock_mc = MagicMock()
|
||||
mock_mc.get_contact_by_key_prefix.return_value = None
|
||||
|
||||
with patch("app.dependencies.radio_manager") as mock_rm, \
|
||||
patch("app.repository.ContactRepository.get_by_key_or_prefix", new_callable=AsyncMock) as mock_get:
|
||||
with (
|
||||
patch("app.dependencies.radio_manager") as mock_rm,
|
||||
patch(
|
||||
"app.repository.ContactRepository.get_by_key_or_prefix", new_callable=AsyncMock
|
||||
) as mock_get,
|
||||
):
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.meshcore = mock_mc
|
||||
mock_get.return_value = None
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post(
|
||||
"/api/messages/direct",
|
||||
json={"destination": "nonexistent", "text": "Hello"}
|
||||
"/api/messages/direct", json={"destination": "nonexistent", "text": "Hello"}
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
@@ -120,7 +128,8 @@ class TestChannelsEndpoint:
|
||||
async def test_create_hashtag_channel_derives_key(self):
|
||||
"""Creating hashtag channel derives key from name and stores in DB."""
|
||||
import hashlib
|
||||
from app.routers.channels import create_channel, CreateChannelRequest
|
||||
|
||||
from app.routers.channels import CreateChannelRequest, create_channel
|
||||
|
||||
with patch("app.routers.channels.ChannelRepository") as mock_repo:
|
||||
mock_repo.upsert = AsyncMock()
|
||||
@@ -145,7 +154,7 @@ class TestChannelsEndpoint:
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_channel_with_explicit_key(self):
|
||||
"""Creating channel with explicit key uses provided key."""
|
||||
from app.routers.channels import create_channel, CreateChannelRequest
|
||||
from app.routers.channels import CreateChannelRequest, create_channel
|
||||
|
||||
with patch("app.routers.channels.ChannelRepository") as mock_repo:
|
||||
mock_repo.upsert = AsyncMock()
|
||||
@@ -177,6 +186,7 @@ class TestPacketsEndpoint:
|
||||
mock_repo.get_undecrypted_count = AsyncMock(return_value=42)
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/packets/undecrypted/count")
|
||||
@@ -191,10 +201,12 @@ class TestReadStateEndpoints:
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_contact_read_updates_timestamp(self):
|
||||
"""Marking contact as read updates last_read_at in database."""
|
||||
import aiosqlite
|
||||
import time
|
||||
from app.repository import ContactRepository
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.database import db
|
||||
from app.repository import ContactRepository
|
||||
|
||||
# Use in-memory database for testing
|
||||
conn = await aiosqlite.connect(":memory:")
|
||||
@@ -222,7 +234,7 @@ class TestReadStateEndpoints:
|
||||
# Insert a test contact
|
||||
await conn.execute(
|
||||
"INSERT INTO contacts (public_key, name) VALUES (?, ?)",
|
||||
("abc123def456789012345678901234567890123456789012345678901234", "TestContact")
|
||||
("abc123def456789012345678901234567890123456789012345678901234", "TestContact"),
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
@@ -253,10 +265,12 @@ class TestReadStateEndpoints:
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_channel_read_updates_timestamp(self):
|
||||
"""Marking channel as read updates last_read_at in database."""
|
||||
import aiosqlite
|
||||
import time
|
||||
from app.repository import ChannelRepository
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.database import db
|
||||
from app.repository import ChannelRepository
|
||||
|
||||
# Use in-memory database for testing
|
||||
conn = await aiosqlite.connect(":memory:")
|
||||
@@ -276,7 +290,7 @@ class TestReadStateEndpoints:
|
||||
# Insert a test channel
|
||||
await conn.execute(
|
||||
"INSERT INTO channels (key, name) VALUES (?, ?)",
|
||||
("0123456789ABCDEF0123456789ABCDEF", "#testchannel")
|
||||
("0123456789ABCDEF0123456789ABCDEF", "#testchannel"),
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
@@ -294,9 +308,7 @@ class TestReadStateEndpoints:
|
||||
assert updated is True
|
||||
|
||||
# Verify the timestamp was set
|
||||
channel = await ChannelRepository.get_by_key(
|
||||
"0123456789ABCDEF0123456789ABCDEF"
|
||||
)
|
||||
channel = await ChannelRepository.get_by_key("0123456789ABCDEF0123456789ABCDEF")
|
||||
assert channel is not None
|
||||
assert channel.last_read_at is not None
|
||||
assert channel.last_read_at >= before_time
|
||||
@@ -308,8 +320,9 @@ class TestReadStateEndpoints:
|
||||
async def test_mark_nonexistent_contact_returns_false(self):
|
||||
"""Marking nonexistent contact returns False."""
|
||||
import aiosqlite
|
||||
from app.repository import ContactRepository
|
||||
|
||||
from app.database import db
|
||||
from app.repository import ContactRepository
|
||||
|
||||
# Use in-memory database for testing
|
||||
conn = await aiosqlite.connect(":memory:")
|
||||
@@ -348,10 +361,13 @@ class TestReadStateEndpoints:
|
||||
"""Mark-read endpoint returns 404 for nonexistent contact."""
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
with patch("app.repository.ContactRepository.get_by_key_or_prefix", new_callable=AsyncMock) as mock_get:
|
||||
with patch(
|
||||
"app.repository.ContactRepository.get_by_key_or_prefix", new_callable=AsyncMock
|
||||
) as mock_get:
|
||||
mock_get.return_value = None
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/api/contacts/nonexistent/mark-read")
|
||||
@@ -363,10 +379,13 @@ class TestReadStateEndpoints:
|
||||
"""Mark-read endpoint returns 404 for nonexistent channel."""
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
with patch("app.repository.ChannelRepository.get_by_key", new_callable=AsyncMock) as mock_get:
|
||||
with patch(
|
||||
"app.repository.ChannelRepository.get_by_key", new_callable=AsyncMock
|
||||
) as mock_get:
|
||||
mock_get.return_value = None
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.post("/api/channels/NONEXISTENT/mark-read")
|
||||
@@ -377,8 +396,10 @@ class TestReadStateEndpoints:
|
||||
@pytest.mark.asyncio
|
||||
async def test_mark_all_read_updates_all_conversations(self):
|
||||
"""Bulk mark-all-read updates all contacts and channels."""
|
||||
import aiosqlite
|
||||
import time
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.database import db
|
||||
|
||||
conn = await aiosqlite.connect(":memory:")
|
||||
@@ -401,8 +422,12 @@ class TestReadStateEndpoints:
|
||||
""")
|
||||
|
||||
# Insert test data with NULL last_read_at
|
||||
await conn.execute("INSERT INTO contacts (public_key, name) VALUES (?, ?)", ("contact1", "Alice"))
|
||||
await conn.execute("INSERT INTO contacts (public_key, name) VALUES (?, ?)", ("contact2", "Bob"))
|
||||
await conn.execute(
|
||||
"INSERT INTO contacts (public_key, name) VALUES (?, ?)", ("contact1", "Alice")
|
||||
)
|
||||
await conn.execute(
|
||||
"INSERT INTO contacts (public_key, name) VALUES (?, ?)", ("contact2", "Bob")
|
||||
)
|
||||
await conn.execute("INSERT INTO channels (key, name) VALUES (?, ?)", ("CHAN1", "#test1"))
|
||||
await conn.execute("INSERT INTO channels (key, name) VALUES (?, ?)", ("CHAN2", "#test2"))
|
||||
await conn.commit()
|
||||
@@ -415,6 +440,7 @@ class TestReadStateEndpoints:
|
||||
|
||||
# Call the endpoint
|
||||
from app.routers.read_state import mark_all_read
|
||||
|
||||
result = await mark_all_read()
|
||||
|
||||
assert result["status"] == "ok"
|
||||
@@ -443,8 +469,9 @@ class TestRawPacketRepository:
|
||||
async def test_create_returns_id_for_new_packet(self):
|
||||
"""First insert of packet data returns a valid ID."""
|
||||
import aiosqlite
|
||||
from app.repository import RawPacketRepository
|
||||
|
||||
from app.database import db
|
||||
from app.repository import RawPacketRepository
|
||||
|
||||
# Use in-memory database for testing
|
||||
conn = await aiosqlite.connect(":memory:")
|
||||
@@ -482,8 +509,9 @@ class TestRawPacketRepository:
|
||||
async def test_different_packets_both_stored(self):
|
||||
"""Different packet data both get stored with unique IDs."""
|
||||
import aiosqlite
|
||||
from app.repository import RawPacketRepository
|
||||
|
||||
from app.database import db
|
||||
from app.repository import RawPacketRepository
|
||||
|
||||
# Use in-memory database for testing
|
||||
conn = await aiosqlite.connect(":memory:")
|
||||
@@ -524,10 +552,12 @@ class TestRawPacketRepository:
|
||||
@pytest.mark.asyncio
|
||||
async def test_prune_old_undecrypted_deletes_old_packets(self):
|
||||
"""Prune deletes undecrypted packets older than specified days."""
|
||||
import aiosqlite
|
||||
import time
|
||||
from app.repository import RawPacketRepository
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.database import db
|
||||
from app.repository import RawPacketRepository
|
||||
|
||||
conn = await aiosqlite.connect(":memory:")
|
||||
conn.row_factory = aiosqlite.Row
|
||||
@@ -551,17 +581,17 @@ class TestRawPacketRepository:
|
||||
# Insert old undecrypted packet
|
||||
await conn.execute(
|
||||
"INSERT INTO raw_packets (timestamp, data, decrypted) VALUES (?, ?, 0)",
|
||||
(old_timestamp, b"\x01\x02\x03")
|
||||
(old_timestamp, b"\x01\x02\x03"),
|
||||
)
|
||||
# Insert recent undecrypted packet
|
||||
await conn.execute(
|
||||
"INSERT INTO raw_packets (timestamp, data, decrypted) VALUES (?, ?, 0)",
|
||||
(recent_timestamp, b"\x04\x05\x06")
|
||||
(recent_timestamp, b"\x04\x05\x06"),
|
||||
)
|
||||
# Insert old but decrypted packet (should NOT be deleted)
|
||||
await conn.execute(
|
||||
"INSERT INTO raw_packets (timestamp, data, decrypted) VALUES (?, ?, 1)",
|
||||
(old_timestamp, b"\x07\x08\x09")
|
||||
(old_timestamp, b"\x07\x08\x09"),
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
@@ -585,10 +615,12 @@ class TestRawPacketRepository:
|
||||
@pytest.mark.asyncio
|
||||
async def test_prune_old_undecrypted_returns_zero_when_nothing_to_delete(self):
|
||||
"""Prune returns 0 when no packets match criteria."""
|
||||
import aiosqlite
|
||||
import time
|
||||
from app.repository import RawPacketRepository
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.database import db
|
||||
from app.repository import RawPacketRepository
|
||||
|
||||
conn = await aiosqlite.connect(":memory:")
|
||||
conn.row_factory = aiosqlite.Row
|
||||
@@ -611,7 +643,7 @@ class TestRawPacketRepository:
|
||||
# Insert only recent packet
|
||||
await conn.execute(
|
||||
"INSERT INTO raw_packets (timestamp, data, decrypted) VALUES (?, ?, 0)",
|
||||
(recent_timestamp, b"\x01\x02\x03")
|
||||
(recent_timestamp, b"\x01\x02\x03"),
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
@@ -633,11 +665,12 @@ class TestMaintenanceEndpoint:
|
||||
@pytest.mark.asyncio
|
||||
async def test_maintenance_prunes_and_vacuums(self):
|
||||
"""Maintenance endpoint prunes old packets and runs vacuum."""
|
||||
import aiosqlite
|
||||
import time
|
||||
from app.repository import RawPacketRepository
|
||||
|
||||
import aiosqlite
|
||||
|
||||
from app.database import db
|
||||
from app.routers.packets import run_maintenance, MaintenanceRequest
|
||||
from app.routers.packets import MaintenanceRequest, run_maintenance
|
||||
|
||||
conn = await aiosqlite.connect(":memory:")
|
||||
conn.row_factory = aiosqlite.Row
|
||||
@@ -660,11 +693,11 @@ class TestMaintenanceEndpoint:
|
||||
# Insert old undecrypted packets
|
||||
await conn.execute(
|
||||
"INSERT INTO raw_packets (timestamp, data, decrypted) VALUES (?, ?, 0)",
|
||||
(old_timestamp, b"\x01\x02\x03")
|
||||
(old_timestamp, b"\x01\x02\x03"),
|
||||
)
|
||||
await conn.execute(
|
||||
"INSERT INTO raw_packets (timestamp, data, decrypted) VALUES (?, ?, 0)",
|
||||
(old_timestamp, b"\x04\x05\x06")
|
||||
(old_timestamp, b"\x04\x05\x06"),
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
@@ -687,16 +720,20 @@ class TestHealthEndpointDatabaseSize:
|
||||
|
||||
def test_health_includes_database_size(self):
|
||||
"""Health endpoint includes database_size_mb field."""
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import patch
|
||||
|
||||
with patch("app.routers.health.radio_manager") as mock_rm, \
|
||||
patch("app.routers.health.os.path.getsize") as mock_getsize:
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
with (
|
||||
patch("app.routers.health.radio_manager") as mock_rm,
|
||||
patch("app.routers.health.os.path.getsize") as mock_getsize,
|
||||
):
|
||||
mock_rm.is_connected = True
|
||||
mock_rm.port = "/dev/ttyUSB0"
|
||||
mock_getsize.return_value = 10 * 1024 * 1024 # 10 MB
|
||||
|
||||
from app.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
response = client.get("/api/health")
|
||||
|
||||
+11
-11
@@ -7,12 +7,9 @@ which is critical for correctly interpreting mesh network messages.
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
import pytest
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
from app.decoder import (
|
||||
DecryptedGroupText,
|
||||
PacketInfo,
|
||||
PayloadType,
|
||||
RouteType,
|
||||
calculate_channel_hash,
|
||||
@@ -113,10 +110,7 @@ class TestGroupTextDecryption:
|
||||
"""Helper to create a valid encrypted GROUP_TEXT payload."""
|
||||
# Build plaintext: timestamp(4) + flags(1) + message + null terminator
|
||||
plaintext = (
|
||||
timestamp.to_bytes(4, "little")
|
||||
+ bytes([flags])
|
||||
+ message.encode("utf-8")
|
||||
+ b"\x00"
|
||||
timestamp.to_bytes(4, "little") + bytes([flags]) + message.encode("utf-8") + b"\x00"
|
||||
)
|
||||
|
||||
# Pad to 16-byte boundary
|
||||
@@ -269,7 +263,9 @@ class TestAdvertisementParsing:
|
||||
result = try_parse_advertisement(packet)
|
||||
|
||||
assert result is not None
|
||||
assert result.public_key == "8576dc7f679b493f9ab5ac316173e1a56d3388bc3ba75f583f63ab0d1ba2a8ab"
|
||||
assert (
|
||||
result.public_key == "8576dc7f679b493f9ab5ac316173e1a56d3388bc3ba75f583f63ab0d1ba2a8ab"
|
||||
)
|
||||
assert result.name == "Can O Mesh 2 🥫"
|
||||
assert result.device_role == 2 # Repeater
|
||||
assert result.timestamp > 0 # Has valid timestamp
|
||||
@@ -295,7 +291,9 @@ class TestAdvertisementParsing:
|
||||
result = try_parse_advertisement(packet)
|
||||
|
||||
assert result is not None
|
||||
assert result.public_key == "ae92564c5c9884854f04f469bbb2bab8871a078053af6cf4aa2c014b18ce8a83"
|
||||
assert (
|
||||
result.public_key == "ae92564c5c9884854f04f469bbb2bab8871a078053af6cf4aa2c014b18ce8a83"
|
||||
)
|
||||
assert result.name == "Flightless🥝"
|
||||
assert result.device_role == 1 # Chat node
|
||||
assert result.timestamp > 0 # Has valid timestamp
|
||||
@@ -321,7 +319,9 @@ class TestAdvertisementParsing:
|
||||
result = try_parse_advertisement(packet)
|
||||
|
||||
assert result is not None
|
||||
assert result.public_key == "2e38c81f7dc0c1cedded6b415b4367cf48f578c5a092ced3490ff0c76efdf1f5"
|
||||
assert (
|
||||
result.public_key == "2e38c81f7dc0c1cedded6b415b4367cf48f578c5a092ced3490ff0c76efdf1f5"
|
||||
)
|
||||
assert result.name == "MennisD"
|
||||
assert result.device_role == 1 # Chat node
|
||||
assert result.timestamp > 0 # Has valid timestamp
|
||||
@@ -330,7 +330,7 @@ class TestAdvertisementParsing:
|
||||
|
||||
def test_parse_advertisement_extracts_public_key(self):
|
||||
"""Advertisement parsing extracts the public key correctly."""
|
||||
from app.decoder import parse_packet, PayloadType
|
||||
from app.decoder import PayloadType, parse_packet
|
||||
|
||||
packet_hex = (
|
||||
"1100AE92564C5C9884854F04F469BBB2BAB8871A078053AF6CF4AA2C014B18CE8A83"
|
||||
|
||||
@@ -86,7 +86,9 @@ class TestRepeatTracking:
|
||||
def test_track_pending_repeat_stores_correctly(self):
|
||||
"""Pending repeats are stored with channel key, text hash, and timestamp."""
|
||||
channel_key = "0123456789ABCDEF0123456789ABCDEF"
|
||||
track_pending_repeat(channel_key=channel_key, text="Hello", timestamp=1700000000, message_id=99)
|
||||
track_pending_repeat(
|
||||
channel_key=channel_key, text="Hello", timestamp=1700000000, message_id=99
|
||||
)
|
||||
|
||||
# Key is (channel_key, text_hash, timestamp)
|
||||
text_hash = str(hash("Hello"))
|
||||
@@ -97,8 +99,18 @@ class TestRepeatTracking:
|
||||
|
||||
def test_same_message_different_channels_tracked_separately(self):
|
||||
"""Same message on different channels creates separate entries."""
|
||||
track_pending_repeat(channel_key="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1", text="Test", timestamp=1000, message_id=1)
|
||||
track_pending_repeat(channel_key="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2", text="Test", timestamp=1000, message_id=2)
|
||||
track_pending_repeat(
|
||||
channel_key="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1",
|
||||
text="Test",
|
||||
timestamp=1000,
|
||||
message_id=1,
|
||||
)
|
||||
track_pending_repeat(
|
||||
channel_key="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA2",
|
||||
text="Test",
|
||||
timestamp=1000,
|
||||
message_id=2,
|
||||
)
|
||||
|
||||
assert len(_pending_repeats) == 2
|
||||
|
||||
@@ -141,8 +153,10 @@ class TestAckEventHandler:
|
||||
track_pending_ack("deadbeef", message_id=123, timeout_ms=10000)
|
||||
|
||||
# Mock dependencies
|
||||
with patch("app.event_handlers.MessageRepository") as mock_repo, \
|
||||
patch("app.event_handlers.broadcast_event") as mock_broadcast:
|
||||
with (
|
||||
patch("app.event_handlers.MessageRepository") as mock_repo,
|
||||
patch("app.event_handlers.broadcast_event") as mock_broadcast,
|
||||
):
|
||||
mock_repo.increment_ack_count = AsyncMock(return_value=1)
|
||||
|
||||
# Create mock event
|
||||
@@ -155,7 +169,9 @@ class TestAckEventHandler:
|
||||
mock_repo.increment_ack_count.assert_called_once_with(123)
|
||||
|
||||
# Verify broadcast sent with ack_count
|
||||
mock_broadcast.assert_called_once_with("message_acked", {"message_id": 123, "ack_count": 1})
|
||||
mock_broadcast.assert_called_once_with(
|
||||
"message_acked", {"message_id": 123, "ack_count": 1}
|
||||
)
|
||||
|
||||
# Verify pending ACK removed
|
||||
assert "deadbeef" not in _pending_acks
|
||||
@@ -167,8 +183,10 @@ class TestAckEventHandler:
|
||||
|
||||
track_pending_ack("expected", message_id=1, timeout_ms=10000)
|
||||
|
||||
with patch("app.event_handlers.MessageRepository") as mock_repo, \
|
||||
patch("app.event_handlers.broadcast_event") as mock_broadcast:
|
||||
with (
|
||||
patch("app.event_handlers.MessageRepository") as mock_repo,
|
||||
patch("app.event_handlers.broadcast_event") as mock_broadcast,
|
||||
):
|
||||
mock_repo.increment_ack_count = AsyncMock()
|
||||
|
||||
class MockEvent:
|
||||
@@ -209,9 +227,11 @@ class TestContactMessageCLIFiltering:
|
||||
"""CLI responses (txt_type=1) are not stored in database."""
|
||||
from app.event_handlers import on_contact_message
|
||||
|
||||
with patch("app.event_handlers.MessageRepository") as mock_repo, \
|
||||
patch("app.event_handlers.ContactRepository") as mock_contact_repo, \
|
||||
patch("app.event_handlers.broadcast_event") as mock_broadcast:
|
||||
with (
|
||||
patch("app.event_handlers.MessageRepository") as mock_repo,
|
||||
patch("app.event_handlers.ContactRepository") as mock_contact_repo,
|
||||
patch("app.event_handlers.broadcast_event") as mock_broadcast,
|
||||
):
|
||||
|
||||
class MockEvent:
|
||||
payload = {
|
||||
@@ -235,10 +255,11 @@ class TestContactMessageCLIFiltering:
|
||||
"""Normal messages (txt_type=0) are still processed normally."""
|
||||
from app.event_handlers import on_contact_message
|
||||
|
||||
with patch("app.event_handlers.MessageRepository") as mock_repo, \
|
||||
patch("app.event_handlers.ContactRepository") as mock_contact_repo, \
|
||||
patch("app.event_handlers.broadcast_event") as mock_broadcast:
|
||||
|
||||
with (
|
||||
patch("app.event_handlers.MessageRepository") as mock_repo,
|
||||
patch("app.event_handlers.ContactRepository") as mock_contact_repo,
|
||||
patch("app.event_handlers.broadcast_event") as mock_broadcast,
|
||||
):
|
||||
mock_repo.create = AsyncMock(return_value=42)
|
||||
mock_contact_repo.get_by_key_prefix = AsyncMock(return_value=None)
|
||||
|
||||
@@ -262,10 +283,11 @@ class TestContactMessageCLIFiltering:
|
||||
"""Messages without txt_type field are treated as normal (not filtered)."""
|
||||
from app.event_handlers import on_contact_message
|
||||
|
||||
with patch("app.event_handlers.MessageRepository") as mock_repo, \
|
||||
patch("app.event_handlers.ContactRepository") as mock_contact_repo, \
|
||||
patch("app.event_handlers.broadcast_event") as mock_broadcast:
|
||||
|
||||
with (
|
||||
patch("app.event_handlers.MessageRepository") as mock_repo,
|
||||
patch("app.event_handlers.ContactRepository") as mock_contact_repo,
|
||||
patch("app.event_handlers.broadcast_event"),
|
||||
):
|
||||
mock_repo.create = AsyncMock(return_value=42)
|
||||
mock_contact_repo.get_by_key_prefix = AsyncMock(return_value=None)
|
||||
|
||||
|
||||
+10
-12
@@ -1,9 +1,9 @@
|
||||
"""Tests for database migrations."""
|
||||
|
||||
import pytest
|
||||
import aiosqlite
|
||||
import pytest
|
||||
|
||||
from app.migrations import get_version, set_version, run_migrations
|
||||
from app.migrations import get_version, run_migrations, set_version
|
||||
|
||||
|
||||
class TestMigrationSystem:
|
||||
@@ -76,24 +76,22 @@ class TestMigration001:
|
||||
# Verify columns exist by inserting and selecting
|
||||
await conn.execute(
|
||||
"INSERT INTO contacts (public_key, name, last_read_at) VALUES (?, ?, ?)",
|
||||
("abc123", "Test", 12345)
|
||||
("abc123", "Test", 12345),
|
||||
)
|
||||
await conn.execute(
|
||||
"INSERT INTO channels (key, name, last_read_at) VALUES (?, ?, ?)",
|
||||
("KEY123", "#test", 67890)
|
||||
("KEY123", "#test", 67890),
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
cursor = await conn.execute(
|
||||
"SELECT last_read_at FROM contacts WHERE public_key = ?",
|
||||
("abc123",)
|
||||
"SELECT last_read_at FROM contacts WHERE public_key = ?", ("abc123",)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
assert row["last_read_at"] == 12345
|
||||
|
||||
cursor = await conn.execute(
|
||||
"SELECT last_read_at FROM channels WHERE key = ?",
|
||||
("KEY123",)
|
||||
"SELECT last_read_at FROM channels WHERE key = ?", ("KEY123",)
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
assert row["last_read_at"] == 67890
|
||||
@@ -186,11 +184,11 @@ class TestMigration001:
|
||||
""")
|
||||
await conn.execute(
|
||||
"INSERT INTO contacts (public_key, name, type) VALUES (?, ?, ?)",
|
||||
("existingkey", "ExistingContact", 1)
|
||||
("existingkey", "ExistingContact", 1),
|
||||
)
|
||||
await conn.execute(
|
||||
"INSERT INTO channels (key, name, is_hashtag) VALUES (?, ?, ?)",
|
||||
("EXISTINGCHAN", "#existing", 1)
|
||||
("EXISTINGCHAN", "#existing", 1),
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
@@ -200,7 +198,7 @@ class TestMigration001:
|
||||
# Verify data is preserved
|
||||
cursor = await conn.execute(
|
||||
"SELECT public_key, name, type, last_read_at FROM contacts WHERE public_key = ?",
|
||||
("existingkey",)
|
||||
("existingkey",),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
assert row["public_key"] == "existingkey"
|
||||
@@ -210,7 +208,7 @@ class TestMigration001:
|
||||
|
||||
cursor = await conn.execute(
|
||||
"SELECT key, name, is_hashtag, last_read_at FROM channels WHERE key = ?",
|
||||
("EXISTINGCHAN",)
|
||||
("EXISTINGCHAN",),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
assert row["key"] == "EXISTINGCHAN"
|
||||
|
||||
@@ -9,13 +9,17 @@ between backend and frontend - both sides test against the same data.
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.database import Database
|
||||
from app.repository import ChannelRepository, MessageRepository, ContactRepository, RawPacketRepository
|
||||
|
||||
from app.repository import (
|
||||
ChannelRepository,
|
||||
ContactRepository,
|
||||
MessageRepository,
|
||||
RawPacketRepository,
|
||||
)
|
||||
|
||||
# Load shared fixtures
|
||||
FIXTURES_PATH = Path(__file__).parent / "fixtures" / "websocket_events.json"
|
||||
@@ -62,7 +66,9 @@ class TestChannelMessagePipeline:
|
||||
"""Test channel message flow: packet → decrypt → store → broadcast."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_channel_message_creates_message_and_broadcasts(self, test_db, captured_broadcasts):
|
||||
async def test_channel_message_creates_message_and_broadcasts(
|
||||
self, test_db, captured_broadcasts
|
||||
):
|
||||
"""A decryptable channel packet creates a message and broadcasts it."""
|
||||
from app.packet_processor import process_raw_packet
|
||||
|
||||
@@ -71,9 +77,7 @@ class TestChannelMessagePipeline:
|
||||
|
||||
# Create the channel in DB first using upsert
|
||||
await ChannelRepository.upsert(
|
||||
key=fixture["channel_key_hex"].upper(),
|
||||
name=fixture["channel_name"],
|
||||
is_hashtag=True
|
||||
key=fixture["channel_key_hex"].upper(), name=fixture["channel_name"], is_hashtag=True
|
||||
)
|
||||
|
||||
broadcasts, mock_broadcast = captured_broadcasts
|
||||
@@ -87,9 +91,7 @@ class TestChannelMessagePipeline:
|
||||
|
||||
# Verify message was stored in database
|
||||
messages = await MessageRepository.get_all(
|
||||
msg_type="CHAN",
|
||||
conversation_key=fixture["channel_key_hex"].upper(),
|
||||
limit=10
|
||||
msg_type="CHAN", conversation_key=fixture["channel_key_hex"].upper(), limit=10
|
||||
)
|
||||
assert len(messages) == 1
|
||||
msg = messages[0]
|
||||
@@ -105,7 +107,9 @@ class TestChannelMessagePipeline:
|
||||
assert broadcast["data"]["type"] == expected["type"]
|
||||
assert broadcast["data"]["conversation_key"] == expected["conversation_key"]
|
||||
assert broadcast["data"]["outgoing"] == expected["outgoing"]
|
||||
assert expected["text"][:30] in broadcast["data"]["text"] # Check text contains expected content
|
||||
assert (
|
||||
expected["text"][:30] in broadcast["data"]["text"]
|
||||
) # Check text contains expected content
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_duplicate_packet_not_broadcast_twice(self, test_db, captured_broadcasts):
|
||||
@@ -118,9 +122,7 @@ class TestChannelMessagePipeline:
|
||||
|
||||
# Create the channel in DB first
|
||||
await ChannelRepository.upsert(
|
||||
key=channel_key_hex,
|
||||
name=fixture["channel_name"],
|
||||
is_hashtag=True
|
||||
key=channel_key_hex, name=fixture["channel_name"], is_hashtag=True
|
||||
)
|
||||
|
||||
broadcasts, mock_broadcast = captured_broadcasts
|
||||
@@ -139,9 +141,7 @@ class TestChannelMessagePipeline:
|
||||
|
||||
# Only ONE message should exist in database
|
||||
messages = await MessageRepository.get_all(
|
||||
msg_type="CHAN",
|
||||
conversation_key=channel_key_hex,
|
||||
limit=10
|
||||
msg_type="CHAN", conversation_key=channel_key_hex, limit=10
|
||||
)
|
||||
assert len(messages) == 1
|
||||
|
||||
@@ -191,7 +191,7 @@ class TestAdvertisementPipeline:
|
||||
|
||||
with patch("app.packet_processor.broadcast_event", mock_broadcast):
|
||||
# Process the advertisement packet through the normal pipeline
|
||||
result = await process_raw_packet(packet_bytes, timestamp=1700000000)
|
||||
await process_raw_packet(packet_bytes, timestamp=1700000000)
|
||||
|
||||
# Verify contact was created in database
|
||||
expected = fixture["expected_ws_event"]["data"]
|
||||
@@ -229,13 +229,15 @@ class TestAdvertisementPipeline:
|
||||
expected = fixture["expected_ws_event"]["data"]
|
||||
|
||||
# Create existing contact with different/missing data
|
||||
await ContactRepository.upsert({
|
||||
"public_key": expected["public_key"],
|
||||
"name": "OldName",
|
||||
"type": 0,
|
||||
"lat": None,
|
||||
"lon": None
|
||||
})
|
||||
await ContactRepository.upsert(
|
||||
{
|
||||
"public_key": expected["public_key"],
|
||||
"name": "OldName",
|
||||
"type": 0,
|
||||
"lat": None,
|
||||
"lon": None,
|
||||
}
|
||||
)
|
||||
|
||||
broadcasts, mock_broadcast = captured_broadcasts
|
||||
|
||||
@@ -255,26 +257,30 @@ class TestAdvertisementPipeline:
|
||||
assert contact.last_path in (None, "")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_advertisement_keeps_shorter_path_within_window(self, test_db, captured_broadcasts):
|
||||
async def test_advertisement_keeps_shorter_path_within_window(
|
||||
self, test_db, captured_broadcasts
|
||||
):
|
||||
"""When receiving echoed advertisements, keep the shortest path within 60s window."""
|
||||
from app.packet_processor import _process_advertisement
|
||||
from app.decoder import parse_packet
|
||||
|
||||
# Create a contact with a longer path (path_len=3)
|
||||
test_pubkey = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
|
||||
await ContactRepository.upsert({
|
||||
"public_key": test_pubkey,
|
||||
"name": "TestNode",
|
||||
"type": 1,
|
||||
"last_seen": 1000,
|
||||
"last_path_len": 3,
|
||||
"last_path": "aabbcc", # 3 bytes = 3 hops
|
||||
})
|
||||
await ContactRepository.upsert(
|
||||
{
|
||||
"public_key": test_pubkey,
|
||||
"name": "TestNode",
|
||||
"type": 1,
|
||||
"last_seen": 1000,
|
||||
"last_path_len": 3,
|
||||
"last_path": "aabbcc", # 3 bytes = 3 hops
|
||||
}
|
||||
)
|
||||
|
||||
# Simulate receiving a shorter path (path_len=1) within 60s
|
||||
# We'll call _process_advertisement directly with mock packet_info
|
||||
from unittest.mock import MagicMock
|
||||
from app.decoder import PacketInfo, RouteType, PayloadType, ParsedAdvertisement
|
||||
|
||||
from app.decoder import ParsedAdvertisement
|
||||
|
||||
broadcasts, mock_broadcast = captured_broadcasts
|
||||
|
||||
@@ -341,7 +347,7 @@ class TestAckPipeline:
|
||||
conversation_key="abc123def456789012345678901234567890123456789012345678901234",
|
||||
sender_timestamp=1700000000,
|
||||
received_at=1700000000,
|
||||
outgoing=True
|
||||
outgoing=True,
|
||||
)
|
||||
|
||||
# Track pending ACK
|
||||
@@ -363,7 +369,7 @@ class TestAckPipeline:
|
||||
messages = await MessageRepository.get_all(
|
||||
msg_type="PRIV",
|
||||
conversation_key="abc123def456789012345678901234567890123456789012345678901234",
|
||||
limit=10
|
||||
limit=10,
|
||||
)
|
||||
assert len(messages) == 1
|
||||
assert messages[0].acked == 1
|
||||
@@ -372,7 +378,6 @@ class TestAckPipeline:
|
||||
ack_broadcasts = [b for b in broadcasts if b["type"] == "message_acked"]
|
||||
assert len(ack_broadcasts) == 1
|
||||
|
||||
expected = FIXTURES["message_acked"]["expected_ws_event"]["data"]
|
||||
broadcast = ack_broadcasts[0]
|
||||
assert "message_id" in broadcast["data"]
|
||||
assert "ack_count" in broadcast["data"]
|
||||
@@ -408,9 +413,7 @@ class TestCreateMessageFromDecrypted:
|
||||
|
||||
# Verify message was stored in database
|
||||
messages = await MessageRepository.get_all(
|
||||
msg_type="CHAN",
|
||||
conversation_key="ABC123DEF456",
|
||||
limit=10
|
||||
msg_type="CHAN", conversation_key="ABC123DEF456", limit=10
|
||||
)
|
||||
assert len(messages) == 1
|
||||
assert messages[0].text == "TestSender: Hello world"
|
||||
@@ -454,9 +457,7 @@ class TestCreateMessageFromDecrypted:
|
||||
|
||||
# Verify text is stored without sender prefix
|
||||
messages = await MessageRepository.get_all(
|
||||
msg_type="CHAN",
|
||||
conversation_key="ABC123DEF456",
|
||||
limit=10
|
||||
msg_type="CHAN", conversation_key="ABC123DEF456", limit=10
|
||||
)
|
||||
assert len(messages) == 1
|
||||
assert messages[0].text == "System message" # No "None: " prefix
|
||||
@@ -509,7 +510,7 @@ class TestCreateMessageFromDecrypted:
|
||||
broadcasts, mock_broadcast = captured_broadcasts
|
||||
|
||||
with patch("app.packet_processor.broadcast_event", mock_broadcast):
|
||||
msg_id = await create_message_from_decrypted(
|
||||
await create_message_from_decrypted(
|
||||
packet_id=packet_id,
|
||||
channel_key="ABC123DEF456",
|
||||
sender="Sender",
|
||||
@@ -537,9 +538,7 @@ class TestMessageBroadcastStructure:
|
||||
channel_key_hex = fixture["channel_key_hex"].upper()
|
||||
|
||||
await ChannelRepository.upsert(
|
||||
key=channel_key_hex,
|
||||
name=fixture["channel_name"],
|
||||
is_hashtag=True
|
||||
key=channel_key_hex, name=fixture["channel_name"], is_hashtag=True
|
||||
)
|
||||
|
||||
broadcasts, mock_broadcast = captured_broadcasts
|
||||
@@ -570,15 +569,13 @@ class TestRawPacketStorage:
|
||||
|
||||
# Create channel so packet can be decrypted
|
||||
await ChannelRepository.upsert(
|
||||
key=channel_key_hex,
|
||||
name=fixture["channel_name"],
|
||||
is_hashtag=True
|
||||
key=channel_key_hex, name=fixture["channel_name"], is_hashtag=True
|
||||
)
|
||||
|
||||
broadcasts, mock_broadcast = captured_broadcasts
|
||||
|
||||
with patch("app.packet_processor.broadcast_event", mock_broadcast):
|
||||
result = await process_raw_packet(packet_bytes, timestamp=1700000000)
|
||||
await process_raw_packet(packet_bytes, timestamp=1700000000)
|
||||
|
||||
# Verify raw_packet broadcast was sent
|
||||
raw_broadcasts = [b for b in broadcasts if b["type"] == "raw_packet"]
|
||||
|
||||
@@ -4,11 +4,11 @@ These tests verify the polling pause mechanism that prevents
|
||||
message polling from interfering with repeater CLI operations.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.radio_sync import (
|
||||
_polling_pause_count,
|
||||
is_polling_paused,
|
||||
pause_polling,
|
||||
sync_radio_time,
|
||||
@@ -19,6 +19,7 @@ from app.radio_sync import (
|
||||
def reset_polling_state():
|
||||
"""Reset polling pause state before and after each test."""
|
||||
import app.radio_sync as radio_sync
|
||||
|
||||
radio_sync._polling_pause_count = 0
|
||||
yield
|
||||
radio_sync._polling_pause_count = 0
|
||||
@@ -143,6 +144,7 @@ class TestSyncRadioTime:
|
||||
# Verify timestamp is reasonable (within last few seconds)
|
||||
call_args = mock_mc.commands.set_time.call_args[0][0]
|
||||
import time
|
||||
|
||||
assert abs(call_args - int(time.time())) < 5
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
||||
Reference in New Issue
Block a user