Files
heartbeat/tests/test_settings_sections.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

109 lines
4.0 KiB
Python

import pytest
from hbd.server import settings as settings_mod
CFG = {
"hbd_port": 50004,
"interval": 20,
"grace": 2,
"users": {
"alice": {"full_name": "Alice Smith", "admin": True, "password": "pbkdf2:sha256:abc",
"notification_channels": ["pushover_ops"]},
},
"oauth": {
"gitea": {"type": "gitea", "url": "https://git.example.com",
"client_id": "cid", "client_secret": "csec", "label": "Sign in with Gitea"},
},
"notification_channels": {
"pushover_ops": {"type": "pushover", "token": "tok", "user": "usr"},
},
"hosts": {},
}
def test_sections_have_section_mode():
sections = settings_mod.get_settings_sections(CFG)
for s in sections:
assert "section_mode" in s, f"Section {s['id']} missing section_mode"
assert s["section_mode"] in ("form", "yaml", "channels")
def test_sections_have_api_section():
sections = settings_mod.get_settings_sections(CFG)
for s in sections:
assert "api_section" in s, f"Section {s['id']} missing api_section"
def test_network_section_has_editable_fields():
sections = settings_mod.get_settings_sections(CFG)
network = next(s for s in sections if s["id"] == "network")
assert network["section_mode"] == "form"
assert network["api_section"] == "server"
editable = [f for f in network["fields"] if f["editable"]]
assert len(editable) >= 2 # hbd_port, ws_port at minimum
def test_yaml_sections_have_correct_mode():
sections = settings_mod.get_settings_sections(CFG)
yaml_sections = {s["id"]: s for s in sections if s["section_mode"] == "yaml"}
assert "channels" not in yaml_sections # now uses "channels" mode
assert "hosts" in yaml_sections
assert "thresholds" in yaml_sections
assert "dns" in yaml_sections
assert yaml_sections["hosts"]["api_section"] == "hosts"
assert yaml_sections["thresholds"]["api_section"] == "thresholds"
assert yaml_sections["dns"]["api_section"] == "dns"
def test_channels_section_uses_channels_mode():
sections = settings_mod.get_settings_sections(CFG)
ch_sec = next(s for s in sections if s["id"] == "channels")
assert ch_sec["section_mode"] == "channels"
assert ch_sec["api_section"] == "notification_channels"
assert len(ch_sec["channels"]) == 1
ch = ch_sec["channels"][0]
assert ch["name"] == "pushover_ops"
assert ch["type"] == "pushover"
assert "owner" in ch
assert "private" in ch
def test_channel_type_schemas_exported():
assert hasattr(settings_mod, "CHANNEL_TYPE_SCHEMAS")
for required_type in ("pushover", "email", "signal", "matrix", "sms_voipms"):
assert required_type in settings_mod.CHANNEL_TYPE_SCHEMAS
schema = settings_mod.CHANNEL_TYPE_SCHEMAS[required_type]
assert "label" in schema
assert "fields" in schema
for f in schema["fields"]:
assert "key" in f
assert "type" in f
assert "required" in f
def test_oauth_section_exists():
sections = settings_mod.get_settings_sections(CFG)
oauth = next((s for s in sections if s["id"] == "oauth"), None)
assert oauth is not None
assert oauth["section_mode"] == "form"
assert oauth["api_section"] == "oauth"
assert len(oauth["providers"]) == 1
assert oauth["providers"][0]["name"] == "gitea"
assert oauth["providers"][0]["client_secret"] == "•••"
def test_all_channel_names_returned():
result = settings_mod.get_settings_data(CFG)
assert "all_channel_names" in result
assert "pushover_ops" in result["all_channel_names"]
def test_users_section_has_user_list():
sections = settings_mod.get_settings_sections(CFG)
users_sec = next(s for s in sections if s["id"] == "users")
assert users_sec["section_mode"] == "form"
assert users_sec["api_section"] == "users"
assert len(users_sec["users"]) == 1
assert users_sec["users"][0]["username"] == "alice"
# Password hash never exposed
assert "password" not in users_sec["users"][0]