""" Discord channel permission math (server-26#225, slice S1). Payloads are shaped like Discord's REST JSON: permission integers are strings, overwrite ``type`` is 0 (role) / 1 (member). """ from datetime import datetime, timedelta, timezone import pytest from app.internal import discord_leases as leases from app.internal import discord_perms as perms from app.internal.discord_perms import ( ADMINISTRATOR, ALL_PERMISSIONS, CONNECT, MOVE_MEMBERS, READ_MESSAGE_HISTORY, SPEAK, VIEW_CHANNEL, check_voice, effective_permissions, parse_permissions, ) GUILD = "100" BOT = "900" OWNER = "1" MUTED = "201" BOTS = "202" DJ = "203" NOW = datetime(2026, 10, 5, 12, 0, tzinfo=timezone.utc) VOICE = VIEW_CHANNEL | CONNECT | SPEAK def guild(everyone=VOICE, extra_roles=()): roles = [{"id": GUILD, "name": "@everyone", "permissions": str(everyone)}] roles += [{"id": rid, "name": name, "permissions": str(p)} for rid, name, p in extra_roles] return {"id": GUILD, "owner_id": OWNER, "roles": roles} def member(roles=(), uid=BOT, timeout=None): return {"user": {"id": uid}, "roles": list(roles), "communication_disabled_until": timeout} def ow(oid, kind, allow=0, deny=0): return {"id": oid, "type": kind, "allow": str(allow), "deny": str(deny)} def channel(overwrites=(), type_=2, user_limit=0, name="dispatch"): return {"id": "500", "name": name, "type": type_, "user_limit": user_limit, "permission_overwrites": list(overwrites)} def eff(g, m, c): return effective_permissions(g, m, c, now=NOW) def test_required_bits_match_radio_bot_invite(): assert VIEW_CHANNEL | CONNECT | SPEAK == leases.RADIO_BOT_PERMISSIONS def test_plain_everyone_grant_is_ok(): bits, trace = eff(guild(), member(), channel()) assert bits == VOICE and trace["Move Members"] r = check_voice(bits, channel(), trace) assert r == {"ok": True, "is_voice": True, "missing": [], "reasons": {}, "user_limit": 0, "limit_bypass": False} def test_owner_gets_everything_despite_denies(): c = channel([ow(GUILD, 0, deny=VOICE), ow(OWNER, 1, deny=VOICE)]) bits, trace = eff(guild(everyone=0), member(uid=OWNER), c) assert bits == ALL_PERMISSIONS and trace == {} def test_administrator_role_bypasses_overwrites(): g = guild(everyone=0, extra_roles=[(BOTS, "Bots", ADMINISTRATOR)]) c = channel([ow(GUILD, 0, deny=VOICE), ow(BOT, 1, deny=VOICE)]) bits, _ = eff(g, member([BOTS]), c) assert bits == ALL_PERMISSIONS assert check_voice(bits, c)["ok"] and check_voice(bits, c)["limit_bypass"] def test_admin_on_everyone_counts_too(): bits, _ = eff(guild(everyone=ADMINISTRATOR), member(), channel()) assert bits == ALL_PERMISSIONS def test_base_is_everyone_or_member_roles_only(): # A role the bot doesn't hold grants nothing. g = guild(everyone=VIEW_CHANNEL, extra_roles=[(DJ, "DJ", CONNECT | SPEAK), (BOTS, "Bots", 0)]) bits, trace = eff(g, member([BOTS]), channel()) assert bits == VIEW_CHANNEL assert check_voice(bits, channel(), trace)["missing"] == ["Connect", "Speak"] assert "not granted by @everyone or any of the bot's roles" in trace["Connect"] bits, _ = eff(g, member([DJ]), channel()) assert bits == VOICE def test_everyone_overwrite_deny_then_allow(): c = channel([ow(GUILD, 0, deny=SPEAK)]) bits, trace = eff(guild(), member(), c) assert check_voice(bits, c, trace)["missing"] == ["Speak"] assert trace["Speak"] == "denied by #dispatch's override for @everyone" # An allow in the same overwrite restores a bit the base lacked. c2 = channel([ow(GUILD, 0, allow=SPEAK)]) bits, _ = eff(guild(everyone=VIEW_CHANNEL | CONNECT), member(), c2) assert bits == VOICE def test_role_overwrite_denial_is_named(): g = guild(extra_roles=[(MUTED, "Muted", 0)]) c = channel([ow(MUTED, 0, deny=SPEAK)]) bits, trace = eff(g, member([MUTED]), c) r = check_voice(bits, c, trace) assert not r["ok"] and r["missing"] == ["Speak"] assert r["reasons"]["Speak"] == "denied by #dispatch's override for @Muted" def test_role_overwrite_for_role_bot_lacks_is_ignored(): g = guild(extra_roles=[(MUTED, "Muted", 0)]) c = channel([ow(MUTED, 0, deny=SPEAK)]) bits, _ = eff(g, member([]), c) assert bits == VOICE def test_role_allow_beats_role_deny_across_roles(): g = guild(extra_roles=[(MUTED, "Muted", 0), (DJ, "DJ", 0)]) c = channel([ow(MUTED, 0, deny=SPEAK), ow(DJ, 0, allow=SPEAK)]) bits, trace = eff(g, member([MUTED, DJ]), c) assert bits & SPEAK and "Speak" not in trace def test_role_overwrites_override_everyone_overwrite(): g = guild(extra_roles=[(DJ, "DJ", 0)]) c = channel([ow(GUILD, 0, deny=CONNECT | SPEAK), ow(DJ, 0, allow=CONNECT | SPEAK)]) bits, trace = eff(g, member([DJ]), c) assert bits == VOICE and trace.get("Connect") is None def test_several_denying_roles_are_all_named(): g = guild(extra_roles=[(MUTED, "Muted", 0), (BOTS, "Bots", 0)]) c = channel([ow(MUTED, 0, deny=CONNECT), ow(BOTS, 0, deny=CONNECT)]) _, trace = eff(g, member([MUTED, BOTS]), c) assert trace["Connect"] == "denied by #dispatch's override for @Muted, @Bots" def test_member_overwrite_applies_last(): g = guild(extra_roles=[(DJ, "DJ", 0)]) c = channel([ow(DJ, 0, allow=SPEAK), ow(BOT, 1, deny=SPEAK)]) bits, trace = eff(g, member([DJ]), c) assert not bits & SPEAK assert trace["Speak"] == "denied by #dispatch's override for the bot itself" # ...and its allow beats a role deny. c2 = channel([ow(GUILD, 0, deny=SPEAK), ow(BOT, 1, allow=SPEAK)]) bits, _ = eff(guild(), member(), c2) assert bits == VOICE def test_member_overwrite_for_another_user_is_ignored(): c = channel([ow("777", 1, deny=VOICE)]) bits, _ = eff(guild(), member(), c) assert bits == VOICE def test_no_view_channel_means_nothing(): c = channel([ow(GUILD, 0, deny=VIEW_CHANNEL)]) bits, trace = eff(guild(everyone=VOICE | MOVE_MEMBERS), member(), c) assert bits == 0 r = check_voice(bits, c, trace) assert r["missing"] == ["View Channel", "Connect", "Speak"] and not r["limit_bypass"] assert trace["View Channel"] == "denied by #dispatch's override for @everyone" assert trace["Connect"] == "the bot can't see #dispatch without View Channel" def test_no_connect_takes_speak_and_move_in_voice(): c = channel([ow(GUILD, 0, deny=CONNECT)]) bits, trace = eff(guild(everyone=VOICE | MOVE_MEMBERS), member(), c) assert bits == VIEW_CHANNEL assert check_voice(bits, c, trace)["missing"] == ["Connect", "Speak"] assert trace["Connect"] == "denied by #dispatch's override for @everyone" assert trace["Speak"] == "Discord removes it in #dispatch without Connect" def test_earlier_cause_survives_later_implicit_denial(): # Speak denied by a role AND later wiped by missing View: the trace keeps # the last step that removed it, which is the View wipe only when Speak # was still present at that point. g = guild(extra_roles=[(MUTED, "Muted", 0)]) c = channel([ow(MUTED, 0, deny=SPEAK), ow(BOT, 1, deny=VIEW_CHANNEL)]) _, trace = eff(g, member([MUTED]), c) assert trace["Speak"] == "denied by #dispatch's override for @Muted" assert trace["Connect"] == "the bot can't see #dispatch without View Channel" def test_timed_out_keeps_only_view_and_history(): future = (NOW + timedelta(hours=1)).isoformat() g = guild(everyone=VOICE | READ_MESSAGE_HISTORY | MOVE_MEMBERS) bits, trace = eff(g, member(timeout=future), channel()) assert bits == VIEW_CHANNEL | READ_MESSAGE_HISTORY assert trace["Connect"] == "the bot is timed out in this server" past = (NOW - timedelta(hours=1)).isoformat().replace("+00:00", "Z") bits, _ = eff(g, member(timeout=past), channel()) assert bits & VOICE == VOICE def test_unparseable_timeout_is_ignored(): bits, _ = eff(guild(), member(timeout="not-a-date"), channel()) assert bits == VOICE def test_permission_strings_above_2_53(): big = (1 << 60) | (1 << 53) | 1 assert parse_permissions(str(big)) == big assert parse_permissions(str((1 << 53) + 1)) == (1 << 53) + 1 # Float rounding would turn this into 2**53 and drop the low bit. assert parse_permissions("9007199254740993") == 9007199254740993 g = guild(everyone=VOICE | (1 << 60)) c = channel([ow(GUILD, 0, allow=1 << 55, deny=1 << 60)]) bits, _ = eff(g, member(), c) assert bits == VOICE | (1 << 55) @pytest.mark.parametrize("value", ["abc", "-5", "1.5", True, 3.0, -1, [], {}]) def test_malformed_permission_values_raise(value): with pytest.raises(ValueError): parse_permissions(value) def test_missing_permission_values_are_zero(): assert parse_permissions(None) == 0 assert parse_permissions("") == 0 assert parse_permissions(7) == 7 def test_malformed_payload_raises_not_silently_denies(): g = guild() g["roles"][0]["permissions"] = "garbage" with pytest.raises(ValueError): eff(g, member(), channel()) def test_legacy_string_overwrite_types(): g = guild(extra_roles=[(MUTED, "Muted", 0)]) c = channel([ow(MUTED, "role", deny=SPEAK), ow(BOT, "member", deny=CONNECT)]) _, trace = eff(g, member([MUTED]), c) assert set(trace) >= {"Connect", "Speak"} def test_unknown_member_role_ids_are_ignored(): bits, _ = eff(guild(), member(["999999"]), channel()) assert bits == VOICE def test_missing_everyone_role_means_empty_base(): g = {"id": GUILD, "owner_id": OWNER, "roles": []} bits, trace = eff(g, member(), channel()) assert bits == 0 and "not granted" in trace["View Channel"] def test_check_voice_only_counts_type_2(): for t in (0, 13, 4): r = check_voice(ALL_PERMISSIONS, channel(type_=t)) assert r["ok"] is False and r["is_voice"] is False and r["missing"] == [] def test_check_voice_user_limit_and_bypass(): c = channel(user_limit=5) assert check_voice(VOICE, c)["user_limit"] == 5 assert check_voice(VOICE, c)["limit_bypass"] is False assert check_voice(VOICE | MOVE_MEMBERS, c)["limit_bypass"] is True assert check_voice(VOICE, channel(user_limit=None))["user_limit"] == 0 def test_labels_match_edge_join_refusal_strings(): # node-26#38 discord_radio._join_refusal emits exactly these labels. assert [label for _, label in perms.REQUIRED_VOICE] == ["View Channel", "Connect", "Speak"] def test_now_defaults_to_current_time(): future = (datetime.now(timezone.utc) + timedelta(days=1)).isoformat() bits, _ = effective_permissions(guild(), member(timeout=future), channel()) assert bits == VIEW_CHANNEL