feat: channel ownership rule — owner-presence means private, admin promote/demote
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NfPpSpccTWBfZg1FTveyaU
This commit is contained in:
@@ -86,61 +86,53 @@ def test_delete_channel_persisted_after_write(tmp_path):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Visibility logic (mirrors http.py _visible_channels_for_user)
|
||||
# Visibility logic (owner-presence rule, hbd.server.config_access)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from hbd.server import config_access as ca # noqa: E402
|
||||
|
||||
|
||||
def _visible(config, user):
|
||||
"""Local copy of the visibility helper for unit testing without the HTTP layer."""
|
||||
all_channels = config.get("notification_channels") or {}
|
||||
if user.get("admin"):
|
||||
return set(all_channels.keys())
|
||||
username = user["username"]
|
||||
return {
|
||||
name for name, cfg in all_channels.items()
|
||||
if isinstance(cfg, dict) and (not cfg.get("private") or cfg.get("owner") == username)
|
||||
}
|
||||
return set(ca.user_channels(all_channels, user["username"]))
|
||||
|
||||
|
||||
CONFIG_VISIBILITY = {
|
||||
"notification_channels": {
|
||||
"pub_ch": {"type": "pushover", "token": "t", "user": "u"},
|
||||
"alice_priv": {"type": "email", "owner": "alice", "private": True,
|
||||
"alice_priv": {"type": "email", "owner": "alice",
|
||||
"recipients": ["a@a.com"], "sender": "s@a.com", "smtp_server": "s"},
|
||||
"bob_priv": {"type": "signal", "owner": "bob", "private": True,
|
||||
"user": "+1", "recipient": "+2"},
|
||||
"admin_owned": {"type": "pushover", "token": "t2", "user": "u2", "owner": "adminuser"},
|
||||
"bob_priv": {"type": "signal", "owner": "bob", "user": "+1", "recipient": "+2"},
|
||||
"stale_flag": {"type": "pushover", "token": "t2", "user": "u2", "private": True},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_public_channel_visible_to_all():
|
||||
def test_global_channel_visible_to_all():
|
||||
for uname in ("alice", "bob", "carol"):
|
||||
user = {"username": uname, "admin": False}
|
||||
assert "pub_ch" in _visible(CONFIG_VISIBILITY, user)
|
||||
assert "pub_ch" in _visible(CONFIG_VISIBILITY, {"username": uname, "admin": False})
|
||||
|
||||
|
||||
def test_private_channel_visible_only_to_owner():
|
||||
def test_owned_channel_visible_only_to_owner():
|
||||
alice = {"username": "alice", "admin": False}
|
||||
bob = {"username": "bob", "admin": False}
|
||||
carol = {"username": "carol", "admin": False}
|
||||
|
||||
bob = {"username": "bob", "admin": False}
|
||||
assert "alice_priv" in _visible(CONFIG_VISIBILITY, alice)
|
||||
assert "alice_priv" not in _visible(CONFIG_VISIBILITY, bob)
|
||||
assert "alice_priv" not in _visible(CONFIG_VISIBILITY, carol)
|
||||
|
||||
assert "bob_priv" in _visible(CONFIG_VISIBILITY, bob)
|
||||
assert "bob_priv" not in _visible(CONFIG_VISIBILITY, alice)
|
||||
|
||||
|
||||
def test_admin_sees_all_channels():
|
||||
admin = {"username": "adminuser", "admin": True}
|
||||
visible = _visible(CONFIG_VISIBILITY, admin)
|
||||
assert visible == {"pub_ch", "alice_priv", "bob_priv", "admin_owned"}
|
||||
assert _visible(CONFIG_VISIBILITY, admin) == {"pub_ch", "alice_priv", "bob_priv", "stale_flag"}
|
||||
|
||||
|
||||
def test_admin_owned_channel_is_public_by_default():
|
||||
def test_stale_private_flag_without_owner_is_global():
|
||||
"""Owner-presence is the single signal; a leftover private flag is ignored."""
|
||||
alice = {"username": "alice", "admin": False}
|
||||
assert "admin_owned" in _visible(CONFIG_VISIBILITY, alice)
|
||||
assert "stale_flag" in _visible(CONFIG_VISIBILITY, alice)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user