From 07ace3412c4040a795584fe65de272ef2d879b22 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 23 Apr 2026 21:57:21 +0200 Subject: [PATCH 1/3] testcase for https://github.com/Kozea/Radicale/issues/2101 --- radicale/tests/test_sharing.py | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index aa355c21..60d1917f 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -48,6 +48,10 @@ class TestSharingApiSanity(BaseTest): encoding: str = self.configuration.get("encoding", "stock") htpasswd = ["owner:ownerpw", "user:userpw", "owner1:owner1pw", "user1:user1pw", + "owner@domain.example:owner@pw", "user@domain.example:user@pw", + "owner.surename@domain.example:owner@pw", "user.surename@domain.example:user@pw", + "owner-surename@domain.example:owner@pw", "user-surename@domain.example:user@pw", + "owner_surename@domain.example:owner@pw", "user_surename@domain.example:user@pw", "us😀er:user😀pw", "owner2:owner2pw", "user2:user2pw"] htpasswd_content = "\n".join(htpasswd) @@ -1098,6 +1102,51 @@ class TestSharingApiSanity(BaseTest): json_dict['PathMapped'] = path_owner _, headers, answer = self._sharing_api_json("map", "create", 409, login="owner:ownerpw", json_dict=json_dict) + def test_sharing_api_map_basic_email(self) -> None: + """share-by-map API basic tests.""" + self.configure({"auth": {"type": "htpasswd", + "htpasswd_filename": self.htpasswd_file_path, + "htpasswd_encryption": "plain"}, + "sharing": { + "type": "csv", + "collection_by_map": "True", + "collection_by_token": "True"}, + "logging": {"request_header_on_debug": "False", + "request_content_on_debug": "True"}, + "rights": {"type": "owner_only"}}) + + json_dict: dict + + for (owner, user) in [ + ("owner@domain.example", "user@domain.example"), + ("owner.surename@domain.example", "user.surename@domain.example"), + ("owner-surename@domain.example", "user-surename@domain.example"), + ("owner_surename@domain.example", "user_surename@domain.example"), + ]: + + path_owner = "/" + owner + "/calendar.ics/" + path_user = "/" + user + "/calendar-owner.ics/" + self.mkcalendar(path_owner, login=owner + ":owner@pw") + + for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)): + self.configure({"sharing": {"permit_create_map": "True"}}) + + logging.info("\n*** test: %s", db_type) + self.configure({"sharing": {"type": db_type}}) + + logging.info("\n*** create map with PathMapped, User, PathOrToken (json) -> 200") + json_dict = {} + json_dict['User'] = user + json_dict['PathOrToken'] = path_user + json_dict['PathMapped'] = path_owner + _, headers, answer = self._sharing_api_json("map", "create", 200, login=owner + ":owner@pw", json_dict=json_dict) + + logging.info("\n*** create map with PathMapped, User, PathOrToken (json) -> 200") + json_dict = {} + json_dict['PathOrToken'] = path_user + _, headers, answer = self._sharing_api_json("map", "enable", 200, login=user + ":user@pw", json_dict=json_dict) + + def test_sharing_api_map_usage(self) -> None: """share-by-map API usage tests.""" self.configure({"auth": {"type": "htpasswd", From e06a35dec95d4d6ef2ece76252badc1048db80e9 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 25 Apr 2026 06:47:08 +0200 Subject: [PATCH 2/3] test/auth: add checks for urldecode_username --- radicale/tests/test_auth.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/radicale/tests/test_auth.py b/radicale/tests/test_auth.py index d2f71cfe..408ad3f0 100644 --- a/radicale/tests/test_auth.py +++ b/radicale/tests/test_auth.py @@ -385,6 +385,16 @@ class TestBaseAuthRequests(BaseTest): self._test_htpasswd("plain", "tmp:bepo", ( ("tmp", "bepo", True), ("tmp@domain.example", "bepo", True), ("tmp1", "bepo", False))) + def test_htpasswd_email_encoding_url_true(self) -> None: + self.configure({"auth": {"urldecode_username": "True"}}) + self._test_htpasswd("plain", "tmp@domain.example:bepo", ( + ("tmp", "bepo", False), ("tmp%40domain.example", "bepo", True), ("tmp@domain.example", "bepo", True))) + + def test_htpasswd_email_encoding_url_false(self) -> None: + self.configure({"auth": {"urldecode_username": "False"}}) + self._test_htpasswd("plain", "tmp@domain.example:bepo", ( + ("tmp", "bepo", False), ("tmp%40domain.example", "bepo", False), ("tmp@domain.example", "bepo", True))) + def test_remote_user(self) -> None: self.configure({"auth": {"type": "remote_user"}}) _, responses = self.propfind("/", """\ From d52d76b50584e21fc1fb7f353395840034715a42 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 25 Apr 2026 07:14:28 +0200 Subject: [PATCH 3/3] test/sharing: extend test_sharing_api_map_basic_email --- radicale/tests/test_sharing.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index 60d1917f..c89b651c 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -27,6 +27,7 @@ import os import re import sys import tempfile +import urllib from typing import Dict, Sequence, Tuple, Union import pytest @@ -1112,6 +1113,7 @@ class TestSharingApiSanity(BaseTest): "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", + "response_content_on_debug": "True", "request_content_on_debug": "True"}, "rights": {"type": "owner_only"}}) @@ -1125,7 +1127,8 @@ class TestSharingApiSanity(BaseTest): ]: path_owner = "/" + owner + "/calendar.ics/" - path_user = "/" + user + "/calendar-owner.ics/" + path_user_base = "/" + user + "/" + path_user = path_user_base + "calendar-owner.ics/" self.mkcalendar(path_owner, login=owner + ":owner@pw") for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)): @@ -1139,13 +1142,28 @@ class TestSharingApiSanity(BaseTest): json_dict['User'] = user json_dict['PathOrToken'] = path_user json_dict['PathMapped'] = path_owner + json_dict['Enabled'] = True + json_dict['Hidden'] = False _, headers, answer = self._sharing_api_json("map", "create", 200, login=owner + ":owner@pw", json_dict=json_dict) - logging.info("\n*** create map with PathMapped, User, PathOrToken (json) -> 200") + logging.info("\n*** enable+unhide (json) -> 200") json_dict = {} json_dict['PathOrToken'] = path_user - _, headers, answer = self._sharing_api_json("map", "enable", 200, login=user + ":user@pw", json_dict=json_dict) + json_dict['Enabled'] = True + json_dict['Hidden'] = False + _, headers, answer = self._sharing_api_json("map", "update", 200, login=user + ":user@pw", json_dict=json_dict) + # check PROPFIND as user + logging.info("\n*** PROPFIND collection user -> ok") + _, responses = self.propfind(path_user_base, """\ + + + +""", login=user + ":user@pw", HTTP_DEPTH="1") + assert len(responses) == 2 + logging.info("response: %r", responses) + response = responses[urllib.parse.quote(path_user)] + assert isinstance(response, dict) def test_sharing_api_map_usage(self) -> None: """share-by-map API usage tests."""