mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-09-17 05:04:19 +00:00
Differentially tested the current parser against the pre-PR one over every template shape shipped in config.ini.example and docs (31 shapes x 3 field sets x 4 message states, 372 comparisons) plus 32 adversarial inputs. That found one regression I had introduced: reading a quote immediately after ':' as the start of a quoted argument voided `prefix_if_nonempty:"`, whose unterminated string rejected the whole placeholder and emitted raw template text. An unterminated quote now falls back to greedy parsing, so a literal quote prefix behaves as it always has. Both cases are pinned by tests. Also covered the shorten_url alias in feed formats, which had none: accepted as a function, chains like shorten, falls back to the original on failure, is seen through by shorten_feed_urls so a link is not shortened twice, and does not swallow an unrelated name that merely starts with it. Reset the warn-once flag in the async no-warning test. It asserted warning was never called while a preceding test could already have tripped the flag, so it would have passed vacuously.
189 lines
6.4 KiB
Python
189 lines
6.4 KiB
Python
"""Unit tests for modules.feed_format (shared FeedManager + web preview path)."""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from unittest.mock import patch
|
|
|
|
from modules.feed_format import (
|
|
apply_feed_field_function,
|
|
clean_feed_html_body,
|
|
feed_format_auto_slots,
|
|
format_feed_message,
|
|
format_relative_timestamp,
|
|
sort_feed_items,
|
|
truncate_to_budget,
|
|
)
|
|
|
|
|
|
class TestFormatRelativeTimestamp:
|
|
def test_none_returns_empty(self):
|
|
assert format_relative_timestamp(None) == ""
|
|
|
|
def test_recent_minutes(self):
|
|
published = datetime.now(timezone.utc) - timedelta(minutes=5)
|
|
assert format_relative_timestamp(published) == "5m ago"
|
|
|
|
def test_now(self):
|
|
published = datetime.now(timezone.utc)
|
|
assert format_relative_timestamp(published) == "now"
|
|
|
|
|
|
class TestAutoSlotsAndBudget:
|
|
def test_auto_slots_finds_field(self):
|
|
slots = feed_format_auto_slots("{emoji} {body|auto} {date}")
|
|
assert len(slots) == 1
|
|
assert slots[0][2] == "body"
|
|
|
|
def test_truncate_to_budget_ellipsis(self):
|
|
assert truncate_to_budget("abcdef", 5) == "ab..."
|
|
|
|
def test_truncate_to_budget_short(self):
|
|
assert truncate_to_budget("hi", 10) == "hi"
|
|
|
|
|
|
class TestCleanHtml:
|
|
def test_strips_tags(self):
|
|
assert "<b>" not in clean_feed_html_body("<b>Hello</b>")
|
|
assert "Hello" in clean_feed_html_body("<b>Hello</b>")
|
|
|
|
def test_br_to_newline(self):
|
|
assert "\n" in clean_feed_html_body("a<br>b")
|
|
|
|
|
|
class TestApplyFieldFunction:
|
|
def test_truncate(self):
|
|
assert apply_feed_field_function("Hello World", "truncate:5") == "Hello..."
|
|
|
|
def test_truncate_hard(self):
|
|
assert apply_feed_field_function("Hello World", "truncate_hard:5") == "Hello"
|
|
|
|
def test_substr(self):
|
|
assert apply_feed_field_function("Hello World", "substr:6,5") == "World"
|
|
|
|
|
|
class TestSortFeedItems:
|
|
def test_sort_desc_by_title(self):
|
|
items = [
|
|
{"title": "a", "raw": {}},
|
|
{"title": "c", "raw": {}},
|
|
{"title": "b", "raw": {}},
|
|
]
|
|
sorted_items = sort_feed_items(items, {"field": "title", "order": "desc"})
|
|
assert [i["title"] for i in sorted_items] == ["c", "b", "a"]
|
|
|
|
|
|
class TestFormatFeedMessage:
|
|
def test_basic_title(self):
|
|
msg = format_feed_message(
|
|
{"title": "Hello", "description": ""},
|
|
"{title}",
|
|
)
|
|
assert msg == "Hello"
|
|
|
|
def test_title_unescapes_html_entities(self):
|
|
msg = format_feed_message(
|
|
{"title": "Foo & Bar's", "description": ""},
|
|
"{title}",
|
|
)
|
|
assert msg == "Foo & Bar's"
|
|
|
|
def test_sanitize_strips_control_chars(self):
|
|
msg = format_feed_message(
|
|
{"title": "Hi\x00there", "description": ""},
|
|
"{title}",
|
|
)
|
|
assert "\x00" not in msg
|
|
assert "Hi" in msg and "there" in msg
|
|
|
|
def test_emoji_heuristic(self):
|
|
msg = format_feed_message(
|
|
{"title": "x", "description": ""},
|
|
"{emoji}",
|
|
feed_name="emergency alerts",
|
|
)
|
|
assert msg == "🚨"
|
|
|
|
def test_emoji_field_overrides(self):
|
|
msg = format_feed_message(
|
|
{"title": "x", "description": "", "emoji": "🔥"},
|
|
"{emoji}",
|
|
feed_name="emergency alerts",
|
|
)
|
|
assert msg == "🔥"
|
|
|
|
def test_auto_fills_budget(self):
|
|
msg = format_feed_message(
|
|
{"title": "T", "description": "abcdefghijklmnop"},
|
|
"X{body|auto}Y",
|
|
max_message_length=10,
|
|
)
|
|
assert msg.startswith("X")
|
|
assert msg.endswith("Y")
|
|
assert len(msg) <= 10
|
|
|
|
@patch("modules.feed_format.shorten_url_sync", return_value="https://v.gd/x")
|
|
def test_shorten_feed_urls_plain_link(self, _mock_shorten):
|
|
msg = format_feed_message(
|
|
{"title": "t", "description": "", "link": "https://example.com/long"},
|
|
"{link}",
|
|
shorten_feed_urls=True,
|
|
config=object(),
|
|
)
|
|
assert msg == "https://v.gd/x"
|
|
|
|
@patch("modules.feed_format.shorten_url_sync", return_value="https://v.gd/x")
|
|
def test_shorten_disabled_keeps_long_link(self, _mock_shorten):
|
|
msg = format_feed_message(
|
|
{"title": "t", "description": "", "link": "https://example.com/long"},
|
|
"{link}",
|
|
shorten_feed_urls=False,
|
|
)
|
|
assert msg == "https://example.com/long"
|
|
|
|
|
|
class TestShortenUrlAlias:
|
|
"""`shorten_url` is the response-template spelling; feed formats accept it too.
|
|
|
|
Without the alias a chain copied from a command response_format hits the unknown
|
|
-function branch and the long URL goes out unshortened, with no warning.
|
|
"""
|
|
|
|
@patch("modules.feed_format.shorten_url_sync", return_value="https://v.gd/x")
|
|
def test_shorten_url_is_accepted_as_a_function(self, mock_shorten):
|
|
out = apply_feed_field_function(
|
|
"https://example.com/long", "shorten_url", config=object()
|
|
)
|
|
assert out == "https://v.gd/x"
|
|
mock_shorten.assert_called_once()
|
|
|
|
@patch("modules.feed_format.shorten_url_sync", return_value="https://v.gd/x")
|
|
def test_shorten_url_chains_like_shorten(self, _mock_shorten):
|
|
assert (
|
|
apply_feed_field_function(
|
|
"https://example.com/long", "shorten_url|truncate:9", config=object()
|
|
)
|
|
== apply_feed_field_function(
|
|
"https://example.com/long", "shorten|truncate:9", config=object()
|
|
)
|
|
)
|
|
|
|
@patch("modules.feed_format.shorten_url_sync", return_value="")
|
|
def test_shorten_url_falls_back_to_the_original(self, _mock_shorten):
|
|
out = apply_feed_field_function(
|
|
"https://example.com/long", "shorten_url", config=object()
|
|
)
|
|
assert out == "https://example.com/long"
|
|
|
|
@patch("modules.feed_format.shorten_url_sync", return_value="https://v.gd/x")
|
|
def test_explicit_shorten_url_is_not_double_shortened(self, mock_shorten):
|
|
"""shorten_feed_urls must see through the alias, as it does through `shorten`."""
|
|
format_feed_message(
|
|
{"title": "t", "description": "", "link": "https://example.com/long"},
|
|
"{link|shorten_url}",
|
|
shorten_feed_urls=True,
|
|
config=object(),
|
|
)
|
|
assert mock_shorten.call_count == 1
|
|
|
|
def test_an_unrelated_name_starting_with_shorten_url_is_untouched(self):
|
|
assert apply_feed_field_function("abc", "shorten_urlish") == "abc"
|