fix: DDNS update token in body, webdav gating, regression tests
Unit Tests / test (push) Successful in 7m25s
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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user