diff --git a/drb-c2-core/app/routers/discord_checks.py b/drb-c2-core/app/routers/discord_checks.py index 39f5131..f3160bc 100644 --- a/drb-c2-core/app/routers/discord_checks.py +++ b/drb-c2-core/app/routers/discord_checks.py @@ -87,6 +87,16 @@ def _channel_state(rows: list[dict], enabled: bool) -> str: return "no_bot" +def _checked_at(snaps: dict) -> str: + """When Discord was last actually asked: the OLDEST cached snapshot behind + this answer (a denied result is cached for 10 min), so the picker's + "Permissions checked N ago" never calls a stale answer fresh (server-26#225 + S4). No snapshot (check off, no member bots) = now.""" + times = [s.fetched_at for s in snaps.values() if s is not None and getattr(s, "fetched_at", None)] + ts = min(times) if times else time.time() + return datetime.fromtimestamp(ts, timezone.utc).isoformat() + + async def voice_check(*, guild_id: str, org_id: Optional[str], node_id: Optional[str], channels: list[dict], channel_id: Optional[str], refresh: bool, with_token_ids: bool) -> dict: @@ -171,7 +181,7 @@ async def voice_check(*, guild_id: str, org_id: Optional[str], node_id: Optional "guild_id": guild_id, "node_id": node_id, "enabled": enabled, - "checked_at": datetime.now(timezone.utc).isoformat(), + "checked_at": _checked_at(snaps), "refreshed": refresh, "channels": out_channels, "invite": invite, diff --git a/drb-c2-core/tests/test_voice_check_age.py b/drb-c2-core/tests/test_voice_check_age.py new file mode 100644 index 0000000..e373028 --- /dev/null +++ b/drb-c2-core/tests/test_voice_check_age.py @@ -0,0 +1,37 @@ +""" +voice-check's ``checked_at`` is when Discord was last asked — the oldest +cached snapshot behind the answer — not the time of the HTTP response, so the +picker's "Permissions checked N ago" footer never calls a 10-minute-old +denied result fresh (server-26#225 S4). +""" +import time +from datetime import datetime + +from app.internal import discord_rest +from app.routers import discord_checks as checks +from tests.test_discord_perm_check import GUILD, OWNER, _snap, _token, fs, run # noqa: F401 (fs = fixture) + + +def _age(out) -> float: + return time.time() - datetime.fromisoformat(out["checked_at"]).timestamp() + + +def test_checked_at_is_the_oldest_snapshot_not_now(fs, monkeypatch): + _token(fs, "t1"); _token(fs, "t2") + ages = {"t1": 30, "t2": 420} + + async def _old(token_doc, guild_id, *, refresh=False): + s = _snap(token_doc["token_id"]) + s.fetched_at = time.time() - (0 if refresh else ages[token_doc["token_id"]]) + return s + monkeypatch.setattr(discord_rest, "guild_snapshot", _old) + + out = run(checks.node_voice_check("n1", GUILD, refresh=0, channel_id=None, decoded=OWNER)) + assert 415 <= _age(out) <= 430 + out = run(checks.node_voice_check("n1", GUILD, refresh=1, channel_id=None, decoded=OWNER)) + assert _age(out) < 5 + + +def test_checked_at_without_snapshots_is_now(fs): + out = run(checks.node_voice_check("n1", GUILD, refresh=0, channel_id=None, decoded=OWNER)) # no bots + assert _age(out) < 5