"""Discord daily-feed post: embed shape, webhook delivery, and the once-a-day guard. No network — httpx.post is stubbed, like the IndexNow tests.""" import datetime import time import pytest import discord import notify def _feed(ranked, generated_at=None, date=None): # Fresh by default so it passes homepage.is_stale(); tests that assert on the # embed timestamp pin generated_at explicitly. return { "generated_at": generated_at if generated_at is not None else time.time(), "date": date or datetime.date.today().isoformat(), "ranked": ranked, } _CARD_HOT = { "display": "Tehran, Iran", "name": "Tehran", "metric": "tmax", "metric_label": "High temp", "value": 104.2, "normal": 96.8, "percentile": 99.0, "pct_label": "99th percentile", "grade": "Near Record", "grade_label": "Near Record hot", "tail": "warm", "date_label": None, } _CARD_COLD = { "display": "Oslo, Norway", "name": "Oslo", "metric": "tmin", "metric_label": "Low temp", "value": 5.0, "normal": 20.0, "percentile": 3.0, "pct_label": "3rd percentile", "grade": "Near Record", "grade_label": "Near Record cold", "tail": "cold", "date_label": "Jul 18", } def test_build_embed_has_numbers_and_cities(): embed = discord.build_embed(_feed([_CARD_HOT, _CARD_COLD], generated_at=1_700_000_000.0)) assert "unusual weather" in embed["title"].lower() desc = embed["description"] assert "Tehran, Iran" in desc and "Oslo, Norway" in desc # Real readings, in both units, with the normal for context. assert "104°F (40°C)" in desc assert "normal 97°F" in desc # The percentile + grade tag rides along. assert "99th percentile" in desc and "Near Record hot" in desc # A non-today card is dated so it can't pass for "right now". assert "(Jul 18)" in desc # Warm top card => warm accent; timestamp from the feed's build time. assert embed["color"] == discord._HOT assert embed["timestamp"].startswith("2023-11-14") def test_embed_accent_follows_the_top_cards_tail(): assert discord.build_embed(_feed([_CARD_COLD]))["color"] == discord._COLD assert discord.build_embed(_feed([_CARD_HOT]))["color"] == discord._HOT def test_build_embed_rejects_empty_feed(): with pytest.raises(ValueError): discord.build_embed(_feed([])) def test_post_disabled_without_webhook(monkeypatch): monkeypatch.setattr(discord, "WEBHOOK_URL", "") posted = [] monkeypatch.setattr(discord.httpx, "post", lambda *a, **k: posted.append(a) or None) assert discord.post_daily_feed(_feed([_CARD_HOT])) is False assert posted == [] # never touched the network class _Resp: def __init__(self, code): self.status_code = code def test_post_sends_embed_to_the_webhook(monkeypatch): monkeypatch.setattr(discord, "WEBHOOK_URL", "https://discord.test/webhook/abc") calls = [] monkeypatch.setattr(discord.httpx, "post", lambda url, json=None, **k: (calls.append((url, json)), _Resp(204))[1]) assert discord.post_daily_feed(_feed([_CARD_HOT])) is True (url, payload), = calls assert url == "https://discord.test/webhook/abc" assert payload["username"] == "Thermograph" assert payload["embeds"][0]["description"].startswith("**Tehran, Iran**") def test_post_is_best_effort_on_http_error(monkeypatch): monkeypatch.setattr(discord, "WEBHOOK_URL", "https://discord.test/webhook/abc") def _boom(*a, **k): raise RuntimeError("network down") monkeypatch.setattr(discord.httpx, "post", _boom) # Never raises; just reports failure. assert discord.post_daily_feed(_feed([_CARD_HOT])) is False def test_post_skips_stale_or_empty_feed(monkeypatch): monkeypatch.setattr(discord, "WEBHOOK_URL", "https://discord.test/webhook/abc") monkeypatch.setattr(discord.httpx, "post", lambda *a, **k: _Resp(204)) # Yesterday's feed is stale -> no post. yesterday = (datetime.date.today() - datetime.timedelta(days=1)).isoformat() assert discord.post_daily_feed(_feed([_CARD_HOT], date=yesterday)) is False # Empty ranked -> no post. assert discord.post_daily_feed(_feed([])) is False def test_daily_guard_posts_once_per_day(monkeypatch): monkeypatch.setattr(notify, "_last_discord_post_day", None) monkeypatch.setattr(discord, "enabled", lambda: True) posts = [] monkeypatch.setattr(discord, "post_daily_feed", lambda: posts.append(1) or True) day1 = datetime.date(2026, 7, 19) day2 = datetime.date(2026, 7, 20) class _D(datetime.date): _t = day1 @classmethod def today(cls): return cls._t monkeypatch.setattr(notify.datetime, "date", _D) notify._maybe_post_discord() notify._maybe_post_discord() # same day -> still one post assert posts == [1] _D._t = day2 notify._maybe_post_discord() # new day -> a second post assert posts == [1, 1] def test_daily_guard_retries_after_a_failed_post(monkeypatch): monkeypatch.setattr(notify, "_last_discord_post_day", None) monkeypatch.setattr(discord, "enabled", lambda: True) outcomes = iter([False, True]) monkeypatch.setattr(discord, "post_daily_feed", lambda: next(outcomes)) # First call fails -> day not marked done -> second call retries and succeeds. notify._maybe_post_discord() notify._maybe_post_discord() assert notify._last_discord_post_day == datetime.date.today().isoformat()