""" Tests for WebDAVBackend and NextcloudBackend (Plan 05-04). TDD RED phase — all tests fail until backend/storage/webdav_backend.py and backend/storage/nextcloud_backend.py are implemented. Covers: - WebDAVBackend subclasses StorageBackend (all 7 abstract methods present) - All 7 methods are async coroutines - SSRF guard: construction with private/localhost URL raises ValueError - presigned_get_url and generate_presigned_put_url raise NotImplementedError - _make_path constructs correct WebDAV path - NextcloudBackend subclasses WebDAVBackend - NextcloudBackend inherits SSRF guard - NextcloudBackend has list_folder (async) """ from __future__ import annotations import inspect import pytest pytestmark = pytest.mark.asyncio class TestWebDAVBackendStructure: """Static structure tests — no network calls required.""" def test_webdav_backend_importable(self): from storage.webdav_backend import WebDAVBackend # noqa: F401 def test_webdav_backend_is_storage_backend_subclass(self): from storage.base import StorageBackend from storage.webdav_backend import WebDAVBackend assert issubclass(WebDAVBackend, StorageBackend) @pytest.mark.parametrize( "method", [ "put_object", "get_object", "delete_object", "presigned_get_url", "health_check", "generate_presigned_put_url", "stat_object", ], ) def test_all_7_methods_are_async(self, method): from storage.webdav_backend import WebDAVBackend fn = getattr(WebDAVBackend, method) assert inspect.iscoroutinefunction(fn), f"{method} is not async" class TestWebDAVBackendSSRF: """SSRF guard tests — construction with blocked URL must raise ValueError.""" def test_localhost_raises(self): from storage.webdav_backend import WebDAVBackend with pytest.raises(ValueError): WebDAVBackend("http://localhost/dav", "user", "pass") def test_127_x_raises(self): from storage.webdav_backend import WebDAVBackend with pytest.raises(ValueError): WebDAVBackend("http://127.0.0.1/dav", "user", "pass") def test_10_x_raises(self): from storage.webdav_backend import WebDAVBackend with pytest.raises(ValueError): WebDAVBackend("http://10.0.0.1/dav", "user", "pass") def test_192_168_raises(self): from storage.webdav_backend import WebDAVBackend with pytest.raises(ValueError): WebDAVBackend("http://192.168.1.1/dav", "user", "pass") def test_169_254_raises(self): from storage.webdav_backend import WebDAVBackend with pytest.raises(ValueError): WebDAVBackend("http://169.254.169.254/dav", "user", "pass") class TestWebDAVBackendNotImplemented: """presigned methods must raise NotImplementedError (D-14).""" async def test_presigned_get_url_raises(self): from storage.webdav_backend import WebDAVBackend backend = WebDAVBackend.__new__(WebDAVBackend) backend._server_url = "https://8.8.8.8/dav" with pytest.raises(NotImplementedError): await backend.presigned_get_url("some/key") async def test_generate_presigned_put_url_raises(self): from storage.webdav_backend import WebDAVBackend backend = WebDAVBackend.__new__(WebDAVBackend) backend._server_url = "https://8.8.8.8/dav" with pytest.raises(NotImplementedError): await backend.generate_presigned_put_url("some/key") class TestWebDAVMakePath: """_make_path must produce percent-encoded WebDAV paths (Pitfall 2).""" def test_make_path_basic(self): from storage.webdav_backend import WebDAVBackend backend = WebDAVBackend.__new__(WebDAVBackend) path = backend._make_path("user-uuid", "doc-uuid", ".pdf") assert path == "docuvault/user-uuid/doc-uuid.pdf" def test_make_path_encodes_special_chars(self): from storage.webdav_backend import WebDAVBackend backend = WebDAVBackend.__new__(WebDAVBackend) # UUIDs are alphanumeric and hyphens — no encoding needed for typical values # This test ensures the encoding is applied (result should not contain raw spaces) path = backend._make_path("user id", "doc id", ".pdf") assert " " not in path assert "user%20id" in path or "user+id" in path or "user%2520id" in path class TestNextcloudBackendStructure: """NextcloudBackend structure tests.""" def test_nextcloud_importable(self): from storage.nextcloud_backend import NextcloudBackend # noqa: F401 def test_nextcloud_is_webdav_subclass(self): from storage.nextcloud_backend import NextcloudBackend from storage.webdav_backend import WebDAVBackend assert issubclass(NextcloudBackend, WebDAVBackend) def test_nextcloud_is_storage_backend_subclass(self): from storage.base import StorageBackend from storage.nextcloud_backend import NextcloudBackend assert issubclass(NextcloudBackend, StorageBackend) @pytest.mark.parametrize( "method", [ "put_object", "get_object", "delete_object", "presigned_get_url", "health_check", "generate_presigned_put_url", "stat_object", ], ) def test_all_7_methods_async_on_nextcloud(self, method): from storage.nextcloud_backend import NextcloudBackend fn = getattr(NextcloudBackend, method) assert inspect.iscoroutinefunction(fn), f"{method} is not async" def test_list_folder_present_and_async(self): from storage.nextcloud_backend import NextcloudBackend assert hasattr(NextcloudBackend, "list_folder") assert inspect.iscoroutinefunction(NextcloudBackend.list_folder) class TestNextcloudBackendSSRF: """SSRF guard inherited from WebDAVBackend.__init__.""" def test_10_x_raises_inherited(self): from storage.nextcloud_backend import NextcloudBackend with pytest.raises(ValueError): NextcloudBackend("http://10.0.0.1/dav", "user", "pass") def test_localhost_raises_inherited(self): from storage.nextcloud_backend import NextcloudBackend with pytest.raises(ValueError): NextcloudBackend("http://localhost/dav", "user", "pass")