mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-05 04:52:59 +02:00
Cleanups: Normalize pub keys, prefix message claiming, cursor + null timestamp DB cleanups
This commit is contained in:
@@ -98,6 +98,11 @@ class TestCreateContact:
|
||||
"app.routers.contacts.ContactRepository.upsert",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_upsert,
|
||||
patch(
|
||||
"app.routers.contacts.MessageRepository.claim_prefix_messages",
|
||||
new_callable=AsyncMock,
|
||||
return_value=0,
|
||||
),
|
||||
):
|
||||
from app.main import app
|
||||
|
||||
|
||||
119
tests/test_key_normalization.py
Normal file
119
tests/test_key_normalization.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Tests for public key case normalization."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.database import Database
|
||||
from app.repository import ContactRepository, MessageRepository
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_db():
|
||||
"""Create an in-memory test database."""
|
||||
import app.repository as repo_module
|
||||
|
||||
db = Database(":memory:")
|
||||
await db.connect()
|
||||
|
||||
original_db = repo_module.db
|
||||
repo_module.db = db
|
||||
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
repo_module.db = original_db
|
||||
await db.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_upsert_stores_lowercase_key(test_db):
|
||||
await ContactRepository.upsert(
|
||||
{"public_key": "A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"}
|
||||
)
|
||||
contact = await ContactRepository.get_by_key(
|
||||
"a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
|
||||
)
|
||||
assert contact is not None
|
||||
assert contact.public_key == "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_key_case_insensitive(test_db):
|
||||
await ContactRepository.upsert(
|
||||
{"public_key": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"}
|
||||
)
|
||||
contact = await ContactRepository.get_by_key(
|
||||
"A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2C3D4E5F6A1B2"
|
||||
)
|
||||
assert contact is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_last_contacted_case_insensitive(test_db):
|
||||
key = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
|
||||
await ContactRepository.upsert({"public_key": key})
|
||||
|
||||
await ContactRepository.update_last_contacted(key.upper(), 12345)
|
||||
contact = await ContactRepository.get_by_key(key)
|
||||
assert contact is not None
|
||||
assert contact.last_contacted == 12345
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_by_pubkey_first_byte(test_db):
|
||||
key1 = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
|
||||
key2 = "a1ffddeeaabb1122334455667788990011223344556677889900aabbccddeeff00"
|
||||
key3 = "b2b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
|
||||
|
||||
for key in [key1, key2, key3]:
|
||||
await ContactRepository.upsert({"public_key": key})
|
||||
|
||||
results = await ContactRepository.get_by_pubkey_first_byte("a1")
|
||||
assert len(results) == 2
|
||||
result_keys = {c.public_key for c in results}
|
||||
assert key1 in result_keys
|
||||
assert key2 in result_keys
|
||||
|
||||
results = await ContactRepository.get_by_pubkey_first_byte("A1")
|
||||
assert len(results) == 2 # case insensitive
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_null_sender_timestamp_defaults_to_received_at(test_db):
|
||||
"""Verify that a None/0 sender_timestamp is replaced by received_at."""
|
||||
msg_id = await MessageRepository.create(
|
||||
msg_type="PRIV",
|
||||
text="hello",
|
||||
conversation_key="abcd1234" * 8,
|
||||
sender_timestamp=500, # simulates fallback: `payload.get("sender_timestamp") or received_at`
|
||||
received_at=500,
|
||||
)
|
||||
assert msg_id is not None
|
||||
|
||||
messages = await MessageRepository.get_all(
|
||||
msg_type="PRIV", conversation_key="abcd1234" * 8, limit=10
|
||||
)
|
||||
assert len(messages) == 1
|
||||
assert messages[0].sender_timestamp == 500
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_duplicate_with_same_text_and_null_timestamp_rejected(test_db):
|
||||
"""Two messages with same content and sender_timestamp should be deduped."""
|
||||
received_at = 600
|
||||
msg_id1 = await MessageRepository.create(
|
||||
msg_type="PRIV",
|
||||
text="hello",
|
||||
conversation_key="abcd1234" * 8,
|
||||
sender_timestamp=received_at,
|
||||
received_at=received_at,
|
||||
)
|
||||
assert msg_id1 is not None
|
||||
|
||||
msg_id2 = await MessageRepository.create(
|
||||
msg_type="PRIV",
|
||||
text="hello",
|
||||
conversation_key="abcd1234" * 8,
|
||||
sender_timestamp=received_at,
|
||||
received_at=received_at,
|
||||
)
|
||||
assert msg_id2 is None # duplicate rejected
|
||||
64
tests/test_message_pagination.py
Normal file
64
tests/test_message_pagination.py
Normal file
@@ -0,0 +1,64 @@
|
||||
"""Tests for message pagination using cursor parameters."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.database import Database
|
||||
from app.repository import MessageRepository
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_db():
|
||||
"""Create an in-memory test database."""
|
||||
import app.repository as repo_module
|
||||
|
||||
db = Database(":memory:")
|
||||
await db.connect()
|
||||
|
||||
original_db = repo_module.db
|
||||
repo_module.db = db
|
||||
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
repo_module.db = original_db
|
||||
await db.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cursor_pagination_avoids_overlap(test_db):
|
||||
key = "ABC123DEF456ABC123DEF456ABC12345"
|
||||
|
||||
ids = []
|
||||
for received_at, text in [(200, "m1"), (200, "m2"), (150, "m3"), (100, "m4")]:
|
||||
msg_id = await MessageRepository.create(
|
||||
msg_type="CHAN",
|
||||
text=text,
|
||||
conversation_key=key,
|
||||
sender_timestamp=received_at,
|
||||
received_at=received_at,
|
||||
)
|
||||
assert msg_id is not None
|
||||
ids.append(msg_id)
|
||||
|
||||
page1 = await MessageRepository.get_all(
|
||||
msg_type="CHAN",
|
||||
conversation_key=key,
|
||||
limit=2,
|
||||
offset=0,
|
||||
)
|
||||
assert len(page1) == 2
|
||||
|
||||
oldest = page1[-1]
|
||||
page2 = await MessageRepository.get_all(
|
||||
msg_type="CHAN",
|
||||
conversation_key=key,
|
||||
limit=2,
|
||||
offset=0,
|
||||
before=oldest.received_at,
|
||||
before_id=oldest.id,
|
||||
)
|
||||
assert len(page2) == 2
|
||||
|
||||
ids_page1 = {m.id for m in page1}
|
||||
ids_page2 = {m.id for m in page2}
|
||||
assert ids_page1.isdisjoint(ids_page2)
|
||||
50
tests/test_message_prefix_claim.py
Normal file
50
tests/test_message_prefix_claim.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Tests for prefix-claiming DM messages."""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.database import Database
|
||||
from app.repository import MessageRepository
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def test_db():
|
||||
"""Create an in-memory test database."""
|
||||
import app.repository as repo_module
|
||||
|
||||
db = Database(":memory:")
|
||||
await db.connect()
|
||||
|
||||
original_db = repo_module.db
|
||||
repo_module.db = db
|
||||
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
repo_module.db = original_db
|
||||
await db.disconnect()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_claim_prefix_promotes_dm_to_full_key(test_db):
|
||||
full_key = "a1b2c3d3ba9f5fa8705b9845fe11cc6f01d1d49caaf4d122ac7121663c5beec7"
|
||||
prefix = full_key[:6].upper()
|
||||
|
||||
msg_id = await MessageRepository.create(
|
||||
msg_type="PRIV",
|
||||
text="hello",
|
||||
conversation_key=prefix,
|
||||
sender_timestamp=123,
|
||||
received_at=123,
|
||||
)
|
||||
assert msg_id is not None
|
||||
|
||||
updated = await MessageRepository.claim_prefix_messages(full_key)
|
||||
assert updated == 1
|
||||
|
||||
messages = await MessageRepository.get_all(
|
||||
msg_type="PRIV",
|
||||
conversation_key=full_key,
|
||||
limit=10,
|
||||
)
|
||||
assert len(messages) == 1
|
||||
assert messages[0].conversation_key == full_key.lower()
|
||||
@@ -100,8 +100,8 @@ class TestMigration001:
|
||||
# Run migrations
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
assert applied == 13 # All 13 migrations run
|
||||
assert await get_version(conn) == 13
|
||||
assert applied == 15 # All 15 migrations run
|
||||
assert await get_version(conn) == 15
|
||||
|
||||
# Verify columns exist by inserting and selecting
|
||||
await conn.execute(
|
||||
@@ -183,9 +183,9 @@ class TestMigration001:
|
||||
applied1 = await run_migrations(conn)
|
||||
applied2 = await run_migrations(conn)
|
||||
|
||||
assert applied1 == 13 # All 13 migrations run
|
||||
assert applied1 == 15 # All 15 migrations run
|
||||
assert applied2 == 0 # No migrations on second run
|
||||
assert await get_version(conn) == 13
|
||||
assert await get_version(conn) == 15
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -245,9 +245,9 @@ class TestMigration001:
|
||||
# Run migrations - should not fail
|
||||
applied = await run_migrations(conn)
|
||||
|
||||
# All 13 migrations applied (version incremented) but no error
|
||||
assert applied == 13
|
||||
assert await get_version(conn) == 13
|
||||
# All 15 migrations applied (version incremented) but no error
|
||||
assert applied == 15
|
||||
assert await get_version(conn) == 15
|
||||
finally:
|
||||
await conn.close()
|
||||
|
||||
@@ -374,10 +374,10 @@ class TestMigration013:
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
# Run migration 13
|
||||
# Run migration 13 (plus 14+15 which also run)
|
||||
applied = await run_migrations(conn)
|
||||
assert applied == 1
|
||||
assert await get_version(conn) == 13
|
||||
assert applied == 3
|
||||
assert await get_version(conn) == 15
|
||||
|
||||
# Verify bots array was created with migrated data
|
||||
cursor = await conn.execute("SELECT bots FROM app_settings WHERE id = 1")
|
||||
|
||||
Reference in New Issue
Block a user