feat(12.1-01): repair Nextcloud adapter contract and add URL normalization

- Remove incompatible NextcloudBackend.list_folder(folder_path="") override
  so Nextcloud inherits canonical WebDAVBackend.list_folder(connection_id,
  user_id, parent_ref=None, page_token=None) -> CloudListing
- Add normalize_nextcloud_url() to cloud_utils.py: idempotent canonical DAV
  root derivation, username percent-encoding, https-only, rejects userinfo/
  query/fragment, validates via SSRF guard before return
- Use normalize_nextcloud_url in cloud_backend_factory for nextcloud provider
- Add TestNextcloudBackendNoListFolderOverride and TestNextcloudUrlNormalization
  to test_webdav_backend.py
- All 9 Nextcloud contract failures now pass; full focused suite: 158 passed
This commit is contained in:
curo1305
2026-06-22 08:11:06 +02:00
parent eb68facd6c
commit 2b46f74329
4 changed files with 227 additions and 87 deletions
+103
View File
@@ -186,3 +186,106 @@ class TestNextcloudBackendSSRF:
with pytest.raises(ValueError):
NextcloudBackend("http://localhost/dav", "user", "pass")
class TestNextcloudBackendNoListFolderOverride:
"""Phase 12.1 P0 fix: NextcloudBackend must NOT override list_folder."""
def test_nextcloud_does_not_define_own_list_folder(self):
"""list_folder must not appear in NextcloudBackend.__dict__ (no override)."""
from storage.nextcloud_backend import NextcloudBackend
assert "list_folder" not in NextcloudBackend.__dict__, (
"NextcloudBackend must not define its own list_folder override. "
"Remove the incompatible legacy method so Nextcloud inherits "
"the canonical WebDAVBackend.list_folder implementation."
)
def test_nextcloud_list_folder_is_same_as_webdav(self):
"""NextcloudBackend.list_folder must be the exact WebDAVBackend method."""
from storage.nextcloud_backend import NextcloudBackend
from storage.webdav_backend import WebDAVBackend
assert NextcloudBackend.list_folder is WebDAVBackend.list_folder, (
"NextcloudBackend.list_folder must be inherited from WebDAVBackend "
"— not a re-defined method."
)
def test_nextcloud_signature_matches_webdav(self):
"""Inspect signature — NextcloudBackend.list_folder must accept canonical args."""
import inspect
from storage.nextcloud_backend import NextcloudBackend
sig = inspect.signature(NextcloudBackend.list_folder)
params = list(sig.parameters.keys())
assert "connection_id" in params
assert "user_id" in params
assert "parent_ref" in params
assert "page_token" in params
class TestNextcloudUrlNormalization:
"""Tests for normalize_nextcloud_url helper in cloud_utils."""
def test_bare_origin_normalized(self):
"""Bare https origin normalized to DAV root path."""
from unittest.mock import patch
from storage.cloud_utils import normalize_nextcloud_url
with patch("storage.cloud_utils.validate_cloud_url"):
result = normalize_nextcloud_url("https://nc.example.com", "alice")
assert result == "https://nc.example.com/remote.php/dav/files/alice/"
def test_origin_with_trailing_slash(self):
"""Origin with trailing slash is idempotent in result."""
from unittest.mock import patch
from storage.cloud_utils import normalize_nextcloud_url
with patch("storage.cloud_utils.validate_cloud_url"):
result = normalize_nextcloud_url("https://nc.example.com/", "alice")
assert result == "https://nc.example.com/remote.php/dav/files/alice/"
def test_subpath_preserved(self):
"""Deployment subpath is preserved in canonical URL."""
from unittest.mock import patch
from storage.cloud_utils import normalize_nextcloud_url
with patch("storage.cloud_utils.validate_cloud_url"):
result = normalize_nextcloud_url("https://example.com/nextcloud", "bob")
assert result == "https://example.com/nextcloud/remote.php/dav/files/bob/"
def test_already_canonical_is_idempotent(self):
"""Already canonical URL is returned unchanged."""
from unittest.mock import patch
from storage.cloud_utils import normalize_nextcloud_url
canonical = "https://nc.example.com/remote.php/dav/files/alice/"
with patch("storage.cloud_utils.validate_cloud_url"):
result = normalize_nextcloud_url(canonical, "alice")
assert result == canonical
def test_username_percent_encoded(self):
"""Username with special characters is percent-encoded."""
from unittest.mock import patch
from storage.cloud_utils import normalize_nextcloud_url
with patch("storage.cloud_utils.validate_cloud_url"):
result = normalize_nextcloud_url("https://nc.example.com", "first last")
assert "first%20last" in result
assert " " not in result
def test_rejects_http_scheme(self):
"""http:// URL must raise ValueError (only https allowed for Nextcloud)."""
from storage.cloud_utils import normalize_nextcloud_url
with pytest.raises(ValueError, match="https"):
normalize_nextcloud_url("http://nc.example.com", "alice")
def test_rejects_userinfo(self):
"""URL with user:pass@ in it must raise ValueError."""
from storage.cloud_utils import normalize_nextcloud_url
with pytest.raises(ValueError, match="userinfo"):
normalize_nextcloud_url("https://user:pass@nc.example.com", "alice")
def test_rejects_query_string(self):
"""URL with ?query= must raise ValueError."""
from storage.cloud_utils import normalize_nextcloud_url
with pytest.raises(ValueError, match="query"):
normalize_nextcloud_url("https://nc.example.com?foo=bar", "alice")
def test_rejects_fragment(self):
"""URL with #fragment must raise ValueError."""
from storage.cloud_utils import normalize_nextcloud_url
with pytest.raises(ValueError, match="fragment"):
normalize_nextcloud_url("https://nc.example.com#section", "alice")