MeshChatX/tests/backend/test_local_message_retention.py

295 lines
8.5 KiB
Python

# SPDX-License-Identifier: 0BSD
import time
from unittest.mock import MagicMock
import pytest
from meshchatx.src.backend import local_message_retention as lmr
from meshchatx.src.backend.database import Database
from meshchatx.src.backend.database.provider import DatabaseProvider
from meshchatx.src.backend.database.schema import DatabaseSchema
def test_normalize_unit():
assert lmr.normalize_unit("DAYS") == lmr.UNIT_DAYS
assert lmr.normalize_unit("Month") == lmr.UNIT_MONTHS
assert lmr.normalize_unit("m") == lmr.UNIT_MONTHS
assert lmr.normalize_unit(None) == lmr.UNIT_DAYS
def test_retention_window_seconds():
assert lmr.retention_window_seconds(1, "days") == 86400
assert lmr.retention_window_seconds(2, "months") == 2 * 30 * 86400
assert lmr.retention_window_seconds(20000, "days") == lmr.MAX_VALUE_DAYS * 86400
assert (
lmr.retention_window_seconds(200, "months") == lmr.MAX_VALUE_MONTHS * 30 * 86400
)
def test_local_message_retention_cutoff_ts():
now = 1_000_000.0
c = lmr.local_message_retention_cutoff_ts(now, 1, "days")
assert c == now - 86400.0
@pytest.fixture
def _db_path(tmp_path):
return str(tmp_path / "t.db")
def test_apply_deletes_and_prunes(_db_path):
provider = DatabaseProvider(_db_path)
DatabaseSchema(provider).initialize()
db = Database(_db_path)
now = time.time()
old_ts = now - 10 * 86400
new_ts = now - 86400
peer = "a" * 32
base = {
"source_hash": peer,
"destination_hash": peer,
"peer_hash": peer,
"state": "delivered",
"progress": 1.0,
"is_incoming": 1,
"method": "ephemeral",
"delivery_attempts": 0,
"next_delivery_attempt_at": None,
"title": "t",
"content": "c",
"fields": None,
"rssi": None,
"snr": None,
"quality": None,
"is_spam": 0,
"reply_to_hash": None,
"attachments_stripped": 0,
}
db.messages.upsert_lxmf_message(
{**base, "hash": "a" * 32, "timestamp": old_ts},
)
db.messages.upsert_lxmf_message(
{**base, "hash": "b" * 32, "timestamp": new_ts},
)
db.provider.execute(
"INSERT INTO lxmf_conversation_read_state (destination_hash) VALUES (?)",
(peer,),
)
n = lmr.apply_local_message_retention(
db.messages,
None,
value=2,
unit=lmr.UNIT_DAYS,
now=now,
)
assert n == 1
assert db.messages.count_lxmf_messages() == 1
left = db.provider.fetchone(
"SELECT 1 AS ok FROM lxmf_messages WHERE hash = ?",
("b" * 32,),
)
assert left is not None
rs = db.provider.fetchall(
"SELECT 1 AS ok FROM lxmf_conversation_read_state WHERE destination_hash = ?",
(peer,),
)
assert len(rs) == 1
db.close_all()
provider.close_all()
def test_prune_clears_read_state_when_conversation_empty(_db_path):
provider = DatabaseProvider(_db_path)
DatabaseSchema(provider).initialize()
db = Database(_db_path)
now = time.time()
peer = "c" * 32
h = "d" * 32
old_ts = now - 3 * 86400
db.messages.upsert_lxmf_message(
{
"hash": h,
"source_hash": peer,
"destination_hash": peer,
"peer_hash": peer,
"state": "delivered",
"progress": 1.0,
"is_incoming": 1,
"method": "ephemeral",
"delivery_attempts": 0,
"next_delivery_attempt_at": None,
"title": "t",
"content": "c",
"fields": None,
"timestamp": old_ts,
"rssi": None,
"snr": None,
"quality": None,
"is_spam": 0,
"reply_to_hash": None,
"attachments_stripped": 0,
},
)
db.provider.execute(
"INSERT INTO lxmf_conversation_read_state (destination_hash) VALUES (?)",
(peer,),
)
lmr.apply_local_message_retention(
db.messages,
None,
value=1,
unit=lmr.UNIT_DAYS,
now=now,
)
assert db.messages.count_lxmf_messages() == 0
assert (
len(
db.provider.fetchall(
"SELECT 1 AS x FROM lxmf_conversation_read_state WHERE destination_hash = ?",
(peer,),
),
)
== 0
)
db.close_all()
provider.close_all()
async def test_config_patch_local_message_retention_keys(mock_app):
await mock_app.update_config(
{
"local_message_auto_delete_enabled": True,
"local_message_auto_delete_value": 3,
"local_message_auto_delete_unit": "months",
},
)
assert mock_app.config.local_message_auto_delete_enabled.get() is True
assert mock_app.config.local_message_auto_delete_value.get() == 3
assert mock_app.config.local_message_auto_delete_unit.get() == "months"
await mock_app.update_config(
{
"local_message_auto_delete_value": 9999,
"local_message_auto_delete_unit": "months",
},
)
assert mock_app.config.local_message_auto_delete_value.get() == lmr.MAX_VALUE_MONTHS
def test_apply_calls_cancel_for_hex_hashes(_db_path):
provider = DatabaseProvider(_db_path)
DatabaseSchema(provider).initialize()
db = Database(_db_path)
now = time.time()
h = "aa" * 16
db.messages.upsert_lxmf_message(
{
"hash": h,
"source_hash": h,
"destination_hash": h,
"peer_hash": h,
"state": "sending",
"progress": 0.0,
"is_incoming": 0,
"method": "opportunistic",
"delivery_attempts": 0,
"next_delivery_attempt_at": None,
"title": "",
"content": "x",
"fields": None,
"timestamp": now - 5 * 86400,
"rssi": None,
"snr": None,
"quality": None,
"is_spam": 0,
"reply_to_hash": None,
"attachments_stripped": 0,
},
)
cancel = MagicMock()
lmr.apply_local_message_retention(
db.messages,
cancel,
value=1,
unit="days",
now=now,
)
cancel.assert_called()
assert db.messages.count_lxmf_messages() == 0
db.close_all()
provider.close_all()
def test_parse_before_cutoff_date_and_unix():
assert lmr.parse_before_cutoff("1705276800") == 1705276800.0
assert lmr.parse_before_cutoff("2024-01-15") == 1705276800.0
try:
lmr.parse_before_cutoff("not-a-date")
raise AssertionError("expected ValueError")
except ValueError:
pass
def test_resolve_message_age_cutoff_prefers_before():
now = 2_000_000.0
assert (
lmr.resolve_message_age_cutoff(older_than_days=10, now=now) == now - 10 * 86400
)
assert (
lmr.resolve_message_age_cutoff(
older_than_days=10,
before="2024-01-15",
now=now,
)
== 1705276800.0
)
assert lmr.resolve_message_age_cutoff() is None
try:
lmr.resolve_message_age_cutoff(older_than_days=0)
raise AssertionError("expected ValueError")
except ValueError:
pass
def test_purge_messages_before_cutoff_and_count(_db_path):
provider = DatabaseProvider(_db_path)
DatabaseSchema(provider).initialize()
db = Database(_db_path)
now = time.time()
peer = "c" * 32
base = {
"source_hash": peer,
"destination_hash": peer,
"peer_hash": peer,
"state": "delivered",
"progress": 1.0,
"is_incoming": 1,
"method": "ephemeral",
"delivery_attempts": 0,
"next_delivery_attempt_at": None,
"title": "t",
"content": "c",
"fields": '{"file_attachments":[{"name":"a.bin"}]}',
"rssi": None,
"snr": None,
"quality": None,
"is_spam": 0,
"reply_to_hash": None,
"attachments_stripped": 0,
}
db.messages.upsert_lxmf_message(
{**base, "hash": "d" * 32, "timestamp": now - 40 * 86400}
)
db.messages.upsert_lxmf_message(
{**base, "hash": "e" * 32, "timestamp": now - 5 * 86400}
)
cutoff = now - 30 * 86400
assert db.messages.count_lxmf_messages_with_timestamp_before(cutoff) == 1
rows = db.messages.get_lxmf_messages_with_timestamp_before(cutoff)
assert len(rows) == 1
assert rows[0]["hash"] == "d" * 32
deleted = lmr.purge_messages_before_cutoff(db.messages, None, cutoff)
assert deleted == 1
assert db.messages.count_lxmf_messages() == 1
db.close_all()
provider.close_all()