diff --git a/src/webdav4/client.py b/src/webdav4/client.py index 8c41527..8651466 100644 --- a/src/webdav4/client.py +++ b/src/webdav4/client.py @@ -293,9 +293,11 @@ def options(self, path: str = "") -> Set[str]: detected_features = FeatureDetection(resp) return detected_features.dav_compliances - def join_url(self, path: str) -> URL: + def join_url(self, path: str, add_trailing_slash: bool = False) -> URL: """Join resource path with base url of the webdav server.""" - return join_url(self.base_url, path) + return join_url( + self.base_url, path, add_trailing_slash=add_trailing_slash + ) def propfind( self, path: str, data: str = None, headers: "HeaderTypes" = None @@ -341,13 +343,17 @@ def set_property(self) -> None: """Setting additional property to a resource.""" def _request( - self, method: str, path: str, **kwargs: Any + self, + method: str, + path: str, + add_trailing_slash: bool = False, + **kwargs: Any, ) -> "HTTPResponse": """Internal method for sending request to the server. It handles joining path correctly and checks for common http errors. """ - url = self.join_url(path) + url = self.join_url(path, add_trailing_slash=add_trailing_slash) http_resp = self.http.request(method, url, **kwargs) if http_resp.status_code == HTTPStatus.NOT_FOUND: @@ -448,7 +454,9 @@ def copy( def mkdir(self, path: str) -> None: """Create a collection.""" - call = wrap_fn(self.request, HTTPMethod.MKCOL, path) + call = wrap_fn( + self.request, HTTPMethod.MKCOL, path, add_trailing_slash=True + ) try: http_resp = self.with_retry(call) except HTTPError as exc: diff --git a/src/webdav4/urls.py b/src/webdav4/urls.py index f2e6994..1188d96 100644 --- a/src/webdav4/urls.py +++ b/src/webdav4/urls.py @@ -15,10 +15,15 @@ def normalize_path(path: str) -> str: return strip_leading_slash(path) -def join_url(base_url: URL, path: str) -> URL: +def join_url( + base_url: URL, path: str, add_trailing_slash: bool = False +) -> URL: """Joins base url with a path.""" base_path = base_url.path - return base_url.copy_with(path=join_url_path(base_path, path)) + path = join_url_path(base_path, path) + if add_trailing_slash: + path += "/" + return base_url.copy_with(path=path) def join_url_path(hostname: str, path: str) -> str: diff --git a/tests/test_client.py b/tests/test_client.py index f01b0ac..0a4fbc4 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -24,6 +24,7 @@ ResourceNotFound, ) from webdav4.http import Client as HTTPClient +from webdav4.http import Method from webdav4.http import Method as HTTPMethod from webdav4.urls import URL @@ -402,6 +403,27 @@ def test_mkdir_forbidden_operations(client: Client, server_address: URL): ) +@pytest.mark.parametrize( + "path", ["collections", "/collections", "/collections", "/collections/"] +) +def test_mkdir_sends_a_trailing_slash(path: str): + """Test that mkdir sends a request to the url with a trailing slash. + + See: https://github.com/skshetry/webdav4/issues/55 + """ + from httpx import Request, Response + + response = Response(200, request=Request(Method.MKCOL, "url")) + client = Client( + "http://example.org", http_client=mock.MagicMock(return_value=response) + ) + with mock.patch.object(client.http, "request", return_value=response) as m: + client.mkdir(path) + assert m.call_args == mock.call( + "MKCOL", URL("http://example.org/collections/") + ) + + def test_remove_collection(storage_dir: TmpDir, client: Client): """Test trying to remove a collection resource.""" storage_dir.gen({"data": {"foo": "foo"}})