From 150d1fb9b48dffe21a315eb25f40688f5919cc0d Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Sat, 10 Jan 2026 11:50:13 -0800 Subject: [PATCH] Fix dupe packet test --- app/packet_processor.py | 21 ++++++- app/repository.py | 11 ++-- tests/test_api.py | 129 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 6 deletions(-) diff --git a/app/packet_processor.py b/app/packet_processor.py index 34ec0c9..ca8e47e 100644 --- a/app/packet_processor.py +++ b/app/packet_processor.py @@ -92,7 +92,7 @@ async def create_message_from_decrypted( "txt_type": 0, "signature": None, "outgoing": False, - "acked": False, + "acked": 0, }) return msg_id @@ -131,6 +131,23 @@ async def process_raw_packet( packet_id = await RawPacketRepository.create(raw_bytes, ts) + # If packet_id is None, this is a duplicate packet (same data already exists) + # Skip processing since we've already handled this exact packet + if packet_id is None: + logger.debug("Duplicate raw packet detected, skipping") + return { + "packet_id": None, + "timestamp": ts, + "raw_hex": raw_bytes.hex(), + "payload_type": "Duplicate", + "snr": snr, + "rssi": rssi, + "decrypted": False, + "message_id": None, + "channel_name": None, + "sender": None, + } + raw_hex = raw_bytes.hex() # Parse packet to get type @@ -295,7 +312,7 @@ async def _process_group_text( "txt_type": 0, "signature": None, "outgoing": False, - "acked": False, + "acked": 0, }) # Mark the raw packet as decrypted diff --git a/app/repository.py b/app/repository.py index bde2d70..d9c3c80 100644 --- a/app/repository.py +++ b/app/repository.py @@ -414,16 +414,19 @@ class MessageRepository: class RawPacketRepository: @staticmethod - async def create(data: bytes, timestamp: int | None = None) -> int: - """Create a raw packet. Always stores (no deduplication at this level).""" + async def create(data: bytes, timestamp: int | None = None) -> int | None: + """Create a raw packet. Returns None if duplicate (same data already exists).""" ts = timestamp or int(time.time()) cursor = await db.conn.execute( - "INSERT INTO raw_packets (timestamp, data) VALUES (?, ?)", + "INSERT OR IGNORE INTO raw_packets (timestamp, data) VALUES (?, ?)", (ts, data), ) await db.conn.commit() - return cursor.lastrowid or 0 + # rowcount is 0 if INSERT was ignored due to duplicate, 1 if inserted + if cursor.rowcount == 0: + return None + return cursor.lastrowid @staticmethod async def get_undecrypted_count() -> int: diff --git a/tests/test_api.py b/tests/test_api.py index 78bf9b0..49c1274 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -183,3 +183,132 @@ class TestPacketsEndpoint: assert response.status_code == 200 assert response.json()["count"] == 42 + + +class TestRawPacketRepository: + """Test raw packet storage with deduplication.""" + + @pytest.mark.asyncio + 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 + + # Use in-memory database for testing + conn = await aiosqlite.connect(":memory:") + conn.row_factory = aiosqlite.Row + + # Create the raw_packets table + await conn.execute(""" + CREATE TABLE raw_packets ( + id INTEGER PRIMARY KEY, + timestamp INTEGER NOT NULL, + data BLOB NOT NULL UNIQUE, + decrypted INTEGER DEFAULT 0, + message_id INTEGER, + decrypt_attempts INTEGER DEFAULT 0, + last_attempt INTEGER + ) + """) + await conn.commit() + + # Patch the db._connection to use our test connection + original_conn = db._connection + db._connection = conn + + try: + packet_data = b"\x01\x02\x03\x04\x05" + packet_id = await RawPacketRepository.create(packet_data, 1234567890) + + assert packet_id is not None + assert packet_id > 0 + finally: + db._connection = original_conn + await conn.close() + + @pytest.mark.asyncio + async def test_create_returns_none_for_duplicate_packet(self): + """Second insert of same packet data returns None (duplicate).""" + import aiosqlite + from app.repository import RawPacketRepository + from app.database import db + + # Use in-memory database for testing + conn = await aiosqlite.connect(":memory:") + conn.row_factory = aiosqlite.Row + + # Create the raw_packets table + await conn.execute(""" + CREATE TABLE raw_packets ( + id INTEGER PRIMARY KEY, + timestamp INTEGER NOT NULL, + data BLOB NOT NULL UNIQUE, + decrypted INTEGER DEFAULT 0, + message_id INTEGER, + decrypt_attempts INTEGER DEFAULT 0, + last_attempt INTEGER + ) + """) + await conn.commit() + + # Patch the db._connection to use our test connection + original_conn = db._connection + db._connection = conn + + try: + packet_data = b"\x01\x02\x03\x04\x05" + + # First insert succeeds + first_id = await RawPacketRepository.create(packet_data, 1234567890) + assert first_id is not None + + # Second insert of same data returns None + second_id = await RawPacketRepository.create(packet_data, 1234567891) + assert second_id is None + finally: + db._connection = original_conn + await conn.close() + + @pytest.mark.asyncio + 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 + + # Use in-memory database for testing + conn = await aiosqlite.connect(":memory:") + conn.row_factory = aiosqlite.Row + + # Create the raw_packets table + await conn.execute(""" + CREATE TABLE raw_packets ( + id INTEGER PRIMARY KEY, + timestamp INTEGER NOT NULL, + data BLOB NOT NULL UNIQUE, + decrypted INTEGER DEFAULT 0, + message_id INTEGER, + decrypt_attempts INTEGER DEFAULT 0, + last_attempt INTEGER + ) + """) + await conn.commit() + + # Patch the db._connection to use our test connection + original_conn = db._connection + db._connection = conn + + try: + packet1 = b"\x01\x02\x03" + packet2 = b"\x04\x05\x06" + + id1 = await RawPacketRepository.create(packet1, 1234567890) + id2 = await RawPacketRepository.create(packet2, 1234567891) + + assert id1 is not None + assert id2 is not None + assert id1 != id2 + finally: + db._connection = original_conn + await conn.close()