Files
heartbeat/tests/test_http_users_me.py
T
Andreas Wrede 500d256d76 feat: replace YAML notification channel editor with form-based UI
Notification channels are now managed through a proper web form instead
of a raw YAML textarea. Any authenticated user can create channels; private
channels (owner-scoped) are hidden from other users. The user profile
channel selector becomes a tag/chip picker with a "My Channels" CRUD section.

- settings.py: add CHANNEL_TYPE_SCHEMAS for all 6 notifier types; channel
  section switches to section_mode="channels"; cards include owner/private/min_level
- configio.py: add apply_channel() and delete_channel() for per-entry CRUD
- notify.py: strip owner/private metadata before dispatching to drivers
- http.py: add GET/POST /api/0/notification_channels, PUT/DELETE /{name},
  GET /api/0/notification_channel_types; visibility helper filters private
  channels per user; PUT /api/0/users/me validates against visible channels
- settings.html: card grid with edit/delete per channel; add/edit modal
  with type dropdown and dynamically rendered type-specific fields
- profile.html: chip picker replaces checkbox list; My Channels section
  for creating/editing/deleting user-owned channels
- tests: update test_settings_sections, test_http_users_me; add
  test_notification_channels_api (16 new tests, 46 total passing)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 07:34:26 -04:00

124 lines
4.8 KiB
Python

"""Tests for PUT /api/0/users/me logic."""
import pytest
from hbd.server import users as users_mod
def test_hash_password_roundtrip():
h = users_mod.hash_password("mysecret")
assert h.startswith("pbkdf2:sha256:")
assert users_mod.authenticate.__doc__ is not None # module loaded
def test_password_change_requires_correct_current(tmp_path):
cfg = tmp_path / ".hb.yaml"
initial_hash = users_mod.hash_password("oldpass")
cfg.write_text(
f"hbd_port: 50004\nusers:\n alice:\n full_name: Alice\n admin: true\n password: {initial_hash}\n"
)
users_mod.load_users({"users": {"alice": {"full_name": "Alice", "admin": True, "password": initial_hash}}})
# Correct current password authenticates
assert users_mod.authenticate("alice", "oldpass") is not None
# Wrong current password does not authenticate
assert users_mod.authenticate("alice", "wrongpass") is None
def test_put_users_me_writes_new_fields(tmp_path):
"""Simulate the write path: read config, update user, write back."""
initial_hash = users_mod.hash_password("secret")
yaml_content = (
"hbd_port: 50004\n"
f"users:\n alice:\n full_name: Old Name\n admin: true\n password: {initial_hash}\n"
)
cfg = tmp_path / ".hb.yaml"
cfg.write_text(yaml_content)
from hbd.server import configio
data = configio.read_roundtrip(str(cfg))
# Simulate handler updating full_name and avatar
user_entry = dict(data["users"]["alice"])
user_entry["full_name"] = "New Name"
user_entry["avatar"] = "/img/alice.png"
data["users"]["alice"] = user_entry
configio.write_config(str(cfg), data)
result = configio.read_roundtrip(str(cfg))
assert result["users"]["alice"]["full_name"] == "New Name"
assert result["users"]["alice"]["avatar"] == "/img/alice.png"
assert result["users"]["alice"]["password"] == initial_hash # unchanged
def test_put_users_me_changes_password(tmp_path):
initial_hash = users_mod.hash_password("oldpass")
cfg = tmp_path / ".hb.yaml"
cfg.write_text(
f"hbd_port: 50004\nusers:\n alice:\n full_name: Alice\n password: {initial_hash}\n"
)
from hbd.server import configio
data = configio.read_roundtrip(str(cfg))
new_hash = users_mod.hash_password("newpass")
data["users"]["alice"]["password"] = new_hash
configio.write_config(str(cfg), data)
result = configio.read_roundtrip(str(cfg))
# Load users from new config and authenticate with new password
new_config = {"users": dict(result["users"])}
users_mod.load_users(new_config)
assert users_mod.authenticate("alice", "newpass") is not None
assert users_mod.authenticate("alice", "oldpass") is None
def test_put_users_me_notification_channels(tmp_path):
cfg = tmp_path / ".hb.yaml"
cfg.write_text(
"hbd_port: 50004\n"
"notification_channels:\n pushover_ops:\n type: pushover\n"
"users:\n alice:\n full_name: Alice\n notification_channels: []\n"
)
from hbd.server import configio
data = configio.read_roundtrip(str(cfg))
data["users"]["alice"]["notification_channels"] = ["pushover_ops"]
configio.write_config(str(cfg), data)
result = configio.read_roundtrip(str(cfg))
assert result["users"]["alice"]["notification_channels"] == ["pushover_ops"]
def test_visible_channels_excludes_private_from_others():
"""Private channels owned by another user must not appear in the visible set."""
from hbd.server import settings as settings_mod
config = {
"notification_channels": {
"public_ch": {"type": "pushover", "token": "t", "user": "u"},
"alice_priv": {"type": "email", "owner": "alice", "private": True,
"recipients": ["a@b.com"], "sender": "s@b.com", "smtp_server": "s"},
"bob_priv": {"type": "email", "owner": "bob", "private": True,
"recipients": ["b@b.com"], "sender": "s@b.com", "smtp_server": "s"},
}
}
class FakeUser:
def __init__(self, username, admin=False):
self.username = username
self.admin = admin
alice = FakeUser("alice")
bob = FakeUser("bob")
admin = FakeUser("admin", admin=True)
# Simulate _visible_channels_for_user logic (mirrors http.py implementation)
def visible(user):
all_channels = config.get("notification_channels") or {}
if user.admin:
return set(all_channels.keys())
return {
name for name, cfg in all_channels.items()
if not cfg.get("private") or cfg.get("owner") == user.username
}
assert visible(alice) == {"public_ch", "alice_priv"}
assert visible(bob) == {"public_ch", "bob_priv"}
assert visible(admin) == {"public_ch", "alice_priv", "bob_priv"}