mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-08-02 08:49:39 +00:00
- Added max_posts_per_check to limit the number of items posted per check, defaulting to max_items_per_check for backward compatibility. - Updated logic in FeedManager to process new items, ensuring that only the specified number of posts are made while examining a larger set of items. - Enhanced emoji selection to prioritize per-item emojis from the API, falling back to heuristics based on feed names when absent. - Introduced new truncation and substring functions for formatting messages, improving text handling in feed outputs. - Added API endpoints to reset feed error counts globally or for individual feeds, enhancing error management capabilities in the web viewer.
363 lines
13 KiB
Python
363 lines
13 KiB
Python
"""Tests for modules.db_migrations — MigrationRunner and migration functions."""
|
|
|
|
import logging
|
|
import sqlite3
|
|
|
|
import pytest
|
|
|
|
from modules.db_migrations import (
|
|
MIGRATIONS,
|
|
MigrationRunner,
|
|
_add_column,
|
|
_column_exists,
|
|
_m0017_feed_queue_item_uniqueness,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def conn():
|
|
"""In-memory SQLite connection for migration tests."""
|
|
c = sqlite3.connect(":memory:")
|
|
yield c
|
|
c.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def logger():
|
|
return logging.getLogger("test_migrations")
|
|
|
|
|
|
@pytest.fixture
|
|
def runner(conn, logger):
|
|
return MigrationRunner(conn, logger)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestColumnHelpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestColumnHelpers:
|
|
def test_column_exists_returns_false_for_missing(self, conn):
|
|
conn.execute("CREATE TABLE t (a TEXT)")
|
|
cursor = conn.cursor()
|
|
assert _column_exists(cursor, "t", "b") is False
|
|
|
|
def test_column_exists_returns_true_for_existing(self, conn):
|
|
conn.execute("CREATE TABLE t (a TEXT)")
|
|
cursor = conn.cursor()
|
|
assert _column_exists(cursor, "t", "a") is True
|
|
|
|
def test_add_column_adds_new_column(self, conn):
|
|
conn.execute("CREATE TABLE t (a TEXT)")
|
|
cursor = conn.cursor()
|
|
_add_column(cursor, "t", "b", "INTEGER DEFAULT 0")
|
|
assert _column_exists(cursor, "t", "b") is True
|
|
|
|
def test_add_column_is_idempotent(self, conn):
|
|
conn.execute("CREATE TABLE t (a TEXT)")
|
|
cursor = conn.cursor()
|
|
_add_column(cursor, "t", "a", "TEXT") # already exists — must not raise
|
|
assert _column_exists(cursor, "t", "a") is True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestMigrationRunner
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestMigrationRunner:
|
|
def test_schema_version_table_created_on_run(self, runner, conn):
|
|
runner.run()
|
|
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'")
|
|
assert cursor.fetchone() is not None
|
|
|
|
def test_all_migrations_applied_on_fresh_db(self, runner, conn):
|
|
runner.run()
|
|
cursor = conn.execute("SELECT MAX(version) FROM schema_version")
|
|
assert cursor.fetchone()[0] == len(MIGRATIONS)
|
|
|
|
def test_migrations_recorded_with_descriptions(self, runner, conn):
|
|
runner.run()
|
|
rows = conn.execute("SELECT version, description FROM schema_version ORDER BY version").fetchall()
|
|
assert len(rows) == len(MIGRATIONS)
|
|
for (version, description), (expected_v, expected_d, _) in zip(rows, MIGRATIONS, strict=False):
|
|
assert version == expected_v
|
|
assert description == expected_d
|
|
|
|
def test_run_is_idempotent(self, runner, conn):
|
|
runner.run()
|
|
runner.run() # second call should not re-apply
|
|
cursor = conn.execute("SELECT COUNT(*) FROM schema_version")
|
|
assert cursor.fetchone()[0] == len(MIGRATIONS)
|
|
|
|
def test_non_contiguous_history_applies_missing_versions(self, conn, logger):
|
|
"""If schema_version has gaps, runner should apply missing versions."""
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
version INTEGER NOT NULL,
|
|
description TEXT,
|
|
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
conn.execute("INSERT INTO schema_version (version, description) VALUES (1, 'initial schema')")
|
|
conn.execute("INSERT INTO schema_version (version, description) VALUES (3, 'feed_subscriptions: filter_config, sort_config')")
|
|
|
|
# Seed minimal tables so migrations that ALTER them have something to work with.
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS feed_subscriptions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
feed_url TEXT NOT NULL,
|
|
channel_name TEXT NOT NULL,
|
|
UNIQUE(feed_url, channel_name)
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS channel_operations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
operation_type TEXT NOT NULL
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS feed_message_queue (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
feed_id INTEGER NOT NULL,
|
|
channel_name TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
queued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
sent_at TIMESTAMP
|
|
)
|
|
""")
|
|
conn.commit()
|
|
|
|
runner = MigrationRunner(conn, logger)
|
|
runner.run()
|
|
|
|
applied = {row[0] for row in conn.execute("SELECT version FROM schema_version").fetchall()}
|
|
assert set(range(1, len(MIGRATIONS) + 1)).issubset(applied)
|
|
|
|
def test_partial_history_applies_only_pending(self, conn, logger):
|
|
"""Simulate a DB that already had migration 1 applied."""
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
version INTEGER NOT NULL,
|
|
description TEXT,
|
|
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
conn.execute("INSERT INTO schema_version (version, description) VALUES (1, 'initial schema')")
|
|
conn.commit()
|
|
|
|
# Migration 1 creates feed_subscriptions (among others); inject it manually
|
|
# so later migrations can find the table.
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS feed_subscriptions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
feed_url TEXT NOT NULL,
|
|
channel_name TEXT NOT NULL,
|
|
UNIQUE(feed_url, channel_name)
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS channel_operations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
operation_type TEXT NOT NULL
|
|
)
|
|
""")
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS feed_message_queue (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
feed_id INTEGER NOT NULL,
|
|
channel_name TEXT NOT NULL,
|
|
message TEXT NOT NULL,
|
|
queued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
sent_at TIMESTAMP
|
|
)
|
|
""")
|
|
conn.commit()
|
|
|
|
runner = MigrationRunner(conn, logger)
|
|
runner.run()
|
|
|
|
# All migrations after 1 should now be applied
|
|
cursor = conn.execute("SELECT MAX(version) FROM schema_version")
|
|
assert cursor.fetchone()[0] == len(MIGRATIONS)
|
|
|
|
# Columns from later migrations should exist
|
|
cursor2 = conn.cursor()
|
|
assert _column_exists(cursor2, "feed_subscriptions", "output_format") is True
|
|
assert _column_exists(cursor2, "feed_message_queue", "priority") is True
|
|
|
|
def test_applied_versions_empty_on_empty_table(self, runner, conn):
|
|
runner._ensure_version_table()
|
|
assert runner._applied_versions() == set()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TestSchema — spot-check key tables and columns after full migration
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestSchema:
|
|
def test_geocoding_cache_table_exists(self, runner, conn):
|
|
runner.run()
|
|
cursor = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='geocoding_cache'")
|
|
assert cursor.fetchone() is not None
|
|
|
|
def test_feed_subscriptions_has_output_format(self, runner, conn):
|
|
runner.run()
|
|
cursor = conn.cursor()
|
|
assert _column_exists(cursor, "feed_subscriptions", "output_format") is True
|
|
|
|
def test_feed_subscriptions_has_filter_config(self, runner, conn):
|
|
runner.run()
|
|
cursor = conn.cursor()
|
|
assert _column_exists(cursor, "feed_subscriptions", "filter_config") is True
|
|
|
|
def test_channel_operations_has_result_data(self, runner, conn):
|
|
runner.run()
|
|
cursor = conn.cursor()
|
|
assert _column_exists(cursor, "channel_operations", "result_data") is True
|
|
|
|
def test_channel_operations_has_processed_at(self, runner, conn):
|
|
runner.run()
|
|
cursor = conn.cursor()
|
|
assert _column_exists(cursor, "channel_operations", "processed_at") is True
|
|
|
|
def test_feed_message_queue_has_priority(self, runner, conn):
|
|
runner.run()
|
|
cursor = conn.cursor()
|
|
assert _column_exists(cursor, "feed_message_queue", "priority") is True
|
|
|
|
def test_feed_message_queue_has_item_id(self, runner, conn):
|
|
runner.run()
|
|
cursor = conn.cursor()
|
|
assert _column_exists(cursor, "feed_message_queue", "item_id") is True
|
|
|
|
def test_feed_queue_unique_index_deduplicates_valid_ids(self, conn):
|
|
conn.execute(
|
|
"""
|
|
CREATE TABLE feed_message_queue (
|
|
id INTEGER PRIMARY KEY,
|
|
feed_id INTEGER NOT NULL,
|
|
item_id TEXT,
|
|
sent_at TIMESTAMP
|
|
)
|
|
"""
|
|
)
|
|
conn.executemany(
|
|
"INSERT INTO feed_message_queue VALUES (?, ?, ?, ?)",
|
|
[
|
|
(1, 7, "same", None),
|
|
(2, 7, "same", "2026-01-01"),
|
|
(3, 7, "same", None),
|
|
(4, 7, "", None),
|
|
(5, 7, "", None),
|
|
(6, 7, None, None),
|
|
(7, 7, None, None),
|
|
],
|
|
)
|
|
|
|
_m0017_feed_queue_item_uniqueness(conn.cursor())
|
|
|
|
rows = conn.execute(
|
|
"SELECT id, item_id FROM feed_message_queue ORDER BY id"
|
|
).fetchall()
|
|
assert rows == [(2, "same"), (4, ""), (5, ""), (6, None), (7, None)]
|
|
with pytest.raises(sqlite3.IntegrityError):
|
|
conn.execute(
|
|
"INSERT INTO feed_message_queue (feed_id, item_id) VALUES (7, 'same')"
|
|
)
|
|
conn.execute("INSERT INTO feed_message_queue (feed_id, item_id) VALUES (7, '')")
|
|
|
|
def test_foreign_key_cascade_works_when_enabled(self, runner, conn):
|
|
conn.execute("PRAGMA foreign_keys=ON")
|
|
runner.run()
|
|
|
|
# Create a subscription and activity, then delete subscription.
|
|
sub_id = conn.execute(
|
|
"INSERT INTO feed_subscriptions (feed_type, feed_url, channel_name) VALUES (?, ?, ?)",
|
|
("rss", "https://example.com/feed.xml", "chan"),
|
|
).lastrowid
|
|
conn.execute(
|
|
"INSERT INTO feed_activity (feed_id, item_id, item_title) VALUES (?, ?, ?)",
|
|
(sub_id, "item-1", "title"),
|
|
)
|
|
conn.commit()
|
|
|
|
conn.execute("DELETE FROM feed_subscriptions WHERE id = ?", (sub_id,))
|
|
conn.commit()
|
|
|
|
remaining = conn.execute("SELECT COUNT(*) FROM feed_activity WHERE feed_id = ?", (sub_id,)).fetchone()[0]
|
|
assert remaining == 0
|
|
|
|
def test_repeater_tables_created_by_migrations(self, runner, conn):
|
|
runner.run()
|
|
for table in [
|
|
"repeater_contacts",
|
|
"complete_contact_tracking",
|
|
"daily_stats",
|
|
"unique_advert_packets",
|
|
"purging_log",
|
|
"mesh_connections",
|
|
"observed_paths",
|
|
]:
|
|
cur = conn.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
|
(table,),
|
|
)
|
|
assert cur.fetchone() is not None
|
|
|
|
def test_repeater_indexes_created(self, runner, conn):
|
|
runner.run()
|
|
# Spot-check a few key indexes (not exhaustive).
|
|
for idx in [
|
|
"idx_public_key",
|
|
"idx_complete_public_key",
|
|
"idx_unique_advert_hash",
|
|
"idx_from_prefix",
|
|
"idx_observed_paths_endpoints",
|
|
]:
|
|
cur = conn.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
|
|
(idx,),
|
|
)
|
|
assert cur.fetchone() is not None
|
|
|
|
def test_purging_log_has_details_column_after_full_migration(self, runner, conn):
|
|
runner.run()
|
|
cursor = conn.cursor()
|
|
assert _column_exists(cursor, "purging_log", "details") is True
|
|
|
|
def test_migration_adds_purging_log_details_for_legacy_schema(self, conn, logger):
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS schema_version (
|
|
version INTEGER NOT NULL,
|
|
description TEXT,
|
|
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
""")
|
|
for version, description, _ in MIGRATIONS:
|
|
if version <= 11:
|
|
conn.execute(
|
|
"INSERT INTO schema_version (version, description) VALUES (?, ?)",
|
|
(version, description),
|
|
)
|
|
conn.execute("""
|
|
CREATE TABLE IF NOT EXISTS purging_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
action TEXT NOT NULL,
|
|
public_key TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
reason TEXT
|
|
)
|
|
""")
|
|
conn.commit()
|
|
|
|
runner = MigrationRunner(conn, logger)
|
|
runner.run()
|
|
|
|
cursor = conn.cursor()
|
|
assert _column_exists(cursor, "purging_log", "details") is True
|