fix: DDNS update token in body, webdav gating, regression tests
Unit Tests / test (push) Successful in 7m25s

- PicNgoDDNS.update(): send token in request body instead of Authorization
  header; DDNS server validates it from body (was returning HTTP 422 on
  every heartbeat, leaving IP record stale after fresh install)
- peers.py / Peers.jsx: webdav service_access only valid when 'files' store
  service is installed; was always shown even with no services, confusing
  users into thinking WebDAV was pre-installed
- 10 new regression tests: DDNS update body contract, Caddy always
  regenerates on startup with no services, peer role allowed on
  /api/services/active, webdav gating by installed services

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 16:56:12 -04:00
parent 962d137093
commit 69862331e7
7 changed files with 241 additions and 12 deletions
+106
View File
@@ -515,3 +515,109 @@ def test_create_peer_rolls_back_firewall_on_dns_failure(
finally:
for p in patches:
p.stop()
# ── service_access webdav gating ──────────────────────────────────────────────
def _make_admin_client_with_installed(auth_mgr, mock_email_mgr, mock_calendar_mgr,
mock_file_mgr, mock_wg_mgr, mock_peer_registry,
installed_services):
"""Return patch list for an admin client with get_installed_services pre-configured."""
app.config['TESTING'] = True
app.config['SECRET_KEY'] = 'test-secret'
mock_cfg = MagicMock()
mock_cfg.get_installed_services.return_value = installed_services
patches = [
patch('app.auth_manager', auth_mgr),
patch('app.config_manager', mock_cfg),
patch('app.email_manager', mock_email_mgr),
patch('app.calendar_manager', mock_calendar_mgr),
patch('app.file_manager', mock_file_mgr),
patch('app.wireguard_manager', mock_wg_mgr),
patch('app.peer_registry', mock_peer_registry),
patch('app.firewall_manager'),
]
try:
import auth_routes
patches.append(patch.object(auth_routes, 'auth_manager', auth_mgr, create=True))
except (ImportError, AttributeError):
pass
return patches
def test_webdav_not_offered_when_files_not_installed(
auth_mgr, mock_email_mgr, mock_calendar_mgr,
mock_file_mgr, mock_wg_mgr, mock_peer_registry):
"""Requesting service_access=['webdav'] must fail when files is not installed.
Regression guard: webdav was hardcoded into _valid_services even when
no store services were installed, misleading users into thinking WebDAV
was always available.
"""
patches = _make_admin_client_with_installed(
auth_mgr, mock_email_mgr, mock_calendar_mgr,
mock_file_mgr, mock_wg_mgr, mock_peer_registry,
installed_services={},
)
started = [p.start() for p in patches]
try:
with app.test_client() as client:
_login(client)
resp = _post_peer(client, _peer_payload(service_access=['webdav']))
assert resp.status_code == 400, (
f'expected 400 when requesting webdav without files installed, got {resp.status_code}'
)
data = json.loads(resp.data)
assert 'service_access' in data.get('error', '').lower()
finally:
for p in patches:
p.stop()
def test_webdav_offered_when_files_is_installed(
auth_mgr, mock_email_mgr, mock_calendar_mgr,
mock_file_mgr, mock_wg_mgr, mock_peer_registry):
"""Requesting service_access=['webdav'] must succeed when files is installed."""
patches = _make_admin_client_with_installed(
auth_mgr, mock_email_mgr, mock_calendar_mgr,
mock_file_mgr, mock_wg_mgr, mock_peer_registry,
installed_services={'files': {}},
)
started = [p.start() for p in patches]
try:
with app.test_client() as client:
_login(client)
resp = _post_peer(client, _peer_payload(service_access=['webdav']))
assert resp.status_code == 201, (
f'expected 201 when requesting webdav with files installed, got {resp.status_code}: {resp.data}'
)
finally:
for p in patches:
p.stop()
def test_no_services_installed_peer_gets_empty_service_access(
auth_mgr, mock_email_mgr, mock_calendar_mgr,
mock_file_mgr, mock_wg_mgr, mock_peer_registry):
"""When no store services are installed the default service_access must be empty."""
patches = _make_admin_client_with_installed(
auth_mgr, mock_email_mgr, mock_calendar_mgr,
mock_file_mgr, mock_wg_mgr, mock_peer_registry,
installed_services={},
)
started = [p.start() for p in patches]
try:
with app.test_client() as client:
_login(client)
resp = _post_peer(client, _peer_payload()) # no service_access in payload
assert resp.status_code == 201, (
f'expected 201 with no services installed, got {resp.status_code}: {resp.data}'
)
peer_dict = mock_peer_registry.add_peer.call_args[0][0]
assert peer_dict.get('service_access') == [], (
f"service_access should be [] when no services installed, got {peer_dict.get('service_access')}"
)
finally:
for p in patches:
p.stop()