mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
230 lines
7.6 KiB
Python
230 lines
7.6 KiB
Python
# SPDX-License-Identifier: 0BSD
|
|
|
|
import json
|
|
import time
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from meshchatx.src.backend.database import Database
|
|
from meshchatx.src.backend.database.messages import MessageDAO
|
|
from meshchatx.src.backend.database.provider import DatabaseProvider
|
|
from meshchatx.src.backend.database.schema import DatabaseSchema
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_provider():
|
|
return MagicMock()
|
|
|
|
|
|
@pytest.fixture
|
|
def message_dao(mock_provider):
|
|
return MessageDAO(mock_provider)
|
|
|
|
|
|
@pytest.fixture
|
|
def real_db(tmp_path):
|
|
db_path = str(tmp_path / "messages.db")
|
|
provider = DatabaseProvider(db_path)
|
|
DatabaseSchema(provider).initialize()
|
|
return Database(db_path)
|
|
|
|
|
|
def _insert_message(db, hash_hex, *, is_incoming, state, method="direct"):
|
|
peer = "b" * 32
|
|
db.messages.upsert_lxmf_message(
|
|
{
|
|
"hash": hash_hex,
|
|
"source_hash": peer,
|
|
"destination_hash": peer,
|
|
"peer_hash": peer,
|
|
"state": state,
|
|
"progress": 0.0,
|
|
"is_incoming": is_incoming,
|
|
"method": method,
|
|
"delivery_attempts": 0,
|
|
"next_delivery_attempt_at": None,
|
|
"title": "t",
|
|
"content": "c",
|
|
"fields": None,
|
|
"timestamp": time.time(),
|
|
"rssi": None,
|
|
"snr": None,
|
|
"quality": None,
|
|
"is_spam": 0,
|
|
"reply_to_hash": None,
|
|
"attachments_stripped": 0,
|
|
},
|
|
)
|
|
|
|
|
|
def test_mark_stuck_messages_never_fails_incoming_messages(real_db):
|
|
# Incoming messages are stored with the default "generating" state and must
|
|
# never be flagged as failed by the stuck-message sweep.
|
|
_insert_message(real_db, "a" * 32, is_incoming=1, state="generating")
|
|
# Outbound messages stuck mid-send should be failed.
|
|
_insert_message(real_db, "c" * 32, is_incoming=0, state="generating")
|
|
_insert_message(real_db, "d" * 32, is_incoming=0, state="sending")
|
|
_insert_message(real_db, "e" * 32, is_incoming=0, state="delivered")
|
|
|
|
real_db.messages.mark_stuck_messages_as_failed()
|
|
|
|
assert real_db.messages.get_lxmf_message_by_hash("a" * 32)["state"] == "generating"
|
|
assert real_db.messages.get_lxmf_message_by_hash("c" * 32)["state"] == "failed"
|
|
assert real_db.messages.get_lxmf_message_by_hash("d" * 32)["state"] == "failed"
|
|
assert real_db.messages.get_lxmf_message_by_hash("e" * 32)["state"] == "delivered"
|
|
|
|
|
|
def test_mark_stuck_messages_repairs_previously_failed_incoming(real_db):
|
|
# Simulate a database corrupted by the previous buggy sweep that flipped
|
|
# received messages to "failed".
|
|
_insert_message(real_db, "a" * 32, is_incoming=1, state="failed")
|
|
|
|
real_db.messages.mark_stuck_messages_as_failed()
|
|
|
|
assert real_db.messages.get_lxmf_message_by_hash("a" * 32)["state"] == "generating"
|
|
|
|
|
|
def test_normalize_lxmf_message_for_import_strips_export_metadata():
|
|
row = {
|
|
"id": 1,
|
|
"hash": "a" * 64,
|
|
"source_hash": "b" * 32,
|
|
"destination_hash": "c" * 32,
|
|
"state": "generating",
|
|
"is_incoming": 1,
|
|
"fields": "{}",
|
|
"created_at": "2026-05-09T01:12:37.063720+00:00",
|
|
"lxmf_icon": {"icon_name": "account"},
|
|
}
|
|
normalized = MessageDAO.normalize_lxmf_message_for_import(row)
|
|
assert normalized is not None
|
|
assert normalized["peer_hash"] == "b" * 32
|
|
assert "id" not in normalized
|
|
assert "lxmf_icon" not in normalized
|
|
|
|
|
|
def test_import_lxmf_messages_skips_invalid_rows(real_db):
|
|
messages = [
|
|
{"hash": "a" * 64, "source_hash": "b" * 32, "destination_hash": "c" * 32},
|
|
{"hash": "", "source_hash": "b" * 32, "destination_hash": "c" * 32},
|
|
]
|
|
result = real_db.messages.import_lxmf_messages(messages)
|
|
assert result["imported"] == 1
|
|
assert result["skipped"] == 1
|
|
assert result["errors"] == []
|
|
|
|
|
|
def test_import_lxmf_messages_without_attachments_stripped_column(tmp_path):
|
|
db_path = str(tmp_path / "legacy.db")
|
|
provider = DatabaseProvider(db_path)
|
|
DatabaseSchema(provider).initialize()
|
|
provider.execute("ALTER TABLE lxmf_messages DROP COLUMN attachments_stripped")
|
|
db = Database(db_path)
|
|
|
|
result = db.messages.import_lxmf_messages(
|
|
[
|
|
{
|
|
"id": 1,
|
|
"hash": "a" * 64,
|
|
"source_hash": "b" * 32,
|
|
"destination_hash": "c" * 32,
|
|
"peer_hash": "b" * 32,
|
|
"state": "generating",
|
|
"progress": 0,
|
|
"is_incoming": 1,
|
|
"method": "opportunistic",
|
|
"fields": "{}",
|
|
"timestamp": 1773512977.322906,
|
|
"attachments_stripped": 0,
|
|
},
|
|
],
|
|
)
|
|
assert result["imported"] == 1
|
|
assert result["errors"] == []
|
|
|
|
|
|
def test_upsert_lxmf_message(message_dao, mock_provider):
|
|
mock_provider.fetchall.return_value = [
|
|
{"name": field}
|
|
for field in (
|
|
"hash",
|
|
"source_hash",
|
|
"destination_hash",
|
|
"peer_hash",
|
|
"state",
|
|
"progress",
|
|
"is_incoming",
|
|
"method",
|
|
"delivery_attempts",
|
|
"next_delivery_attempt_at",
|
|
"title",
|
|
"content",
|
|
"fields",
|
|
"timestamp",
|
|
"rssi",
|
|
"snr",
|
|
"quality",
|
|
"is_spam",
|
|
"reply_to_hash",
|
|
"attachments_stripped",
|
|
"path_hops_at_send",
|
|
"path_interface_at_send",
|
|
"path_finding_measure",
|
|
"path_row_hash_hex",
|
|
"created_at",
|
|
"updated_at",
|
|
)
|
|
]
|
|
data = {"hash": "hash1", "content": "hello", "fields": {"key": "val"}}
|
|
message_dao.upsert_lxmf_message(data)
|
|
|
|
args, _ = mock_provider.execute.call_args
|
|
query, params = args
|
|
assert "INSERT INTO lxmf_messages" in query
|
|
assert "hash1" in params
|
|
assert "hello" in params
|
|
assert json.dumps({"key": "val"}) in params
|
|
|
|
|
|
def test_get_lxmf_message_by_hash(message_dao, mock_provider):
|
|
message_dao.get_lxmf_message_by_hash("hash1")
|
|
mock_provider.fetchone.assert_called_with(
|
|
"SELECT * FROM lxmf_messages WHERE hash = ?",
|
|
("hash1",),
|
|
)
|
|
|
|
|
|
def test_delete_lxmf_messages_by_hashes(message_dao, mock_provider):
|
|
message_dao.delete_lxmf_messages_by_hashes(["h1", "h2"])
|
|
args, _ = mock_provider.execute.call_args
|
|
assert "DELETE FROM lxmf_messages WHERE hash IN (?, ?)" in args[0]
|
|
assert args[1] == ("h1", "h2")
|
|
|
|
|
|
def test_delete_all_lxmf_messages(message_dao, mock_provider):
|
|
message_dao.delete_all_lxmf_messages()
|
|
assert mock_provider.execute.call_count == 3
|
|
calls = [call[0][0] for call in mock_provider.execute.call_args_list]
|
|
assert any("DELETE FROM lxmf_messages" in q for q in calls)
|
|
assert any("DELETE FROM lxmf_conversation_read_state" in q for q in calls)
|
|
assert any("DELETE FROM lxmf_conversation_summaries" in q for q in calls)
|
|
|
|
|
|
def test_get_conversation_messages(message_dao, mock_provider):
|
|
message_dao.get_conversation_messages("peer1", limit=10, offset=5)
|
|
mock_provider.fetchall.assert_called_with(
|
|
"SELECT * FROM lxmf_messages WHERE peer_hash = ? ORDER BY timestamp DESC LIMIT ? OFFSET ?",
|
|
("peer1", 10, 5),
|
|
)
|
|
|
|
|
|
def test_set_lxmf_message_path_at_send_if_unset(message_dao, mock_provider):
|
|
message_dao.set_lxmf_message_path_at_send_if_unset("deadbeef", 2, "UDP Interface")
|
|
args, _ = mock_provider.execute.call_args
|
|
query, params = args
|
|
assert "path_hops_at_send" in query
|
|
assert "path_hops_at_send IS NULL" in query
|
|
assert params[0] == 2
|
|
assert params[1] == "UDP Interface"
|
|
assert params[3] == "deadbeef"
|