diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 801f6719..6e2ce07b 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -34,9 +34,6 @@ from radicale.log import logger INTERNAL_TYPES: Sequence[str] = ("csv", "files", "none") DB_FIELDS_V1: Sequence[str] = ('ShareType', 'PathOrToken', 'PathMapped', 'Owner', 'User', 'Permissions', 'EnabledByOwner', 'EnabledByUser', 'HiddenByOwner', 'HiddenByUser', 'TimestampCreated', 'TimestampUpdated', 'Properties') -DB_FIELDS_V1_BOOL: Sequence[str] = ('EnabledByOwner', 'EnabledByUser', 'HiddenByOwner', 'HiddenByUser') -DB_FIELDS_V1_INT: Sequence[str] = ('TimestampCreated', 'TimestampUpdated') -DB_FIELDS_V1_USER_PERMITTED: Sequence[str] = ('EnabledByUser', 'HiddenByUser', 'Properties') # ShareType: # PathOrToken: [PrimaryKey] # PathMapped: @@ -49,9 +46,28 @@ DB_FIELDS_V1_USER_PERMITTED: Sequence[str] = ('EnabledByUser', 'HiddenByUser', ' # HiddenByUser: True|False (share exposure controlled by user) - check skipped if Owner==User # TimestampCreated: (when created) # TimestampUpdated: (last update) -# Properties: Overlay of collection properties +# Properties: Overlay of collection properties in JSON + +DB_TYPES_V1: dict[str, type] = { + "ShareType": str, + "PathOrToken": str, + "PathMapped": str, + "Owner": str, + "User": str, + "Permissions": str, + "EnabledByOwner": bool, + "HiddenByOwner": bool, + "EnabledByUser": bool, + "HiddenByUser": bool, + "TimestampCreated": int, + "TimestampUpdated": int, + "Properties": dict +} + +DB_FIELDS_V1_USER_PERMITTED: Sequence[str] = ('EnabledByUser', 'HiddenByUser', 'Properties') SHARE_TYPES: Sequence[str] = ('token', 'map', 'all') + SHARE_TYPES_V1: Sequence[str] = ('token', 'map') # token: share by secret token (does not require authentication) # map : share by mapping collection of one user to another as virtual @@ -80,13 +96,13 @@ API_TYPES_V1: dict[str, type] = { "PermittedCreateCollectionByToken": bool, "ShareType": str, "PathOrToken": str, - "PathMapped:": str, + "PathMapped": str, "Owner": str, "User": str, "Permissions": str, "Enabled": bool, "Hidden": bool, - "Properties": str} + "Properties": dict} TOKEN_PATTERN_V1: str = "(v1/[a-zA-Z0-9_=\\-]{44})" @@ -261,6 +277,16 @@ class BaseSharing: with self._storage.acquire_lock("r"): for entry in self.database_list_sharing(): logger.debug("analyze: %r", entry) + + # check type + for fieldname in entry: + if fieldname not in DB_TYPES_V1: + logger.error("sharing database row error, unsupported fieldname found: %r", fieldname) + return False + if type(entry[fieldname]) is not DB_TYPES_V1[fieldname]: + logger.error("sharing database entry type error fieldname=%r is %r should %r entry=%r", fieldname, type(fieldname), DB_TYPES_V1[fieldname], entry) + return False + if entry['ShareType'] not in SHARE_TYPES_V1: logger.error("ShareType not supported: %r", entry['ShareType']) return False @@ -274,6 +300,14 @@ class BaseSharing: else: pass + # permissions + try: + # test + config.rights_permission(entry['Permissions']) + except ValueError: + logger.error("Permissions contain invalid entry: %r", entry['Permissions']) + return False + # check PathMapped exists with self._storage.acquire_lock("r", path=entry['PathMapped']): item = next(iter(self._storage.discover(entry['PathMapped'])), None) @@ -921,6 +955,29 @@ class BaseSharing: if share is None: return httputils.NOT_FOUND + if 'Properties' in request_data: + if Properties is None: + # clear properties + Properties = {} + elif Properties == {}: + # empty, nothing to do + pass + elif share['Properties'] is not None: + # replace properties + for prop in share['Properties']: + if logger.isEnabledFor(logging.DEBUG): + logger.debug("TRACE/" + api_info + ": check for existing property %r", prop) + if prop not in Properties: + # overtake + if logger.isEnabledFor(logging.DEBUG): + logger.debug("TRACE/" + api_info + ": overtake property %r", prop) + Properties[prop] = share['Properties'][prop] + elif Properties[prop] == '': + # unset, do nothing + if logger.isEnabledFor(logging.DEBUG): + logger.debug("TRACE/" + api_info + ": clear property %r", prop) + del Properties[prop] + if user == share['Owner']: if PathMapped is not None: # check access Permissions @@ -929,10 +986,6 @@ class BaseSharing: logger.warning(api_info + ": access to %r not allowed for user %r", PathMapped, user) return httputils.NOT_ALLOWED - if 'Properties' in request_data and Properties is None: - # clear properties - Properties = {} - result = self.database_update_sharing( ShareType=ShareType, PathMapped=PathMapped, @@ -967,10 +1020,6 @@ class BaseSharing: return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED - if 'Properties' in request_data and Properties is None: - # clear properties - Properties = {} - # limited update as user result = self.database_update_sharing( ShareType=ShareType, diff --git a/radicale/sharing/csv.py b/radicale/sharing/csv.py index 62c8a9dc..4d65de5e 100644 --- a/radicale/sharing/csv.py +++ b/radicale/sharing/csv.py @@ -15,6 +15,7 @@ # along with Radicale. If not, see . import csv +import json import logging import os from typing import Union @@ -406,21 +407,39 @@ class Sharing(sharing.BaseSharing): return False # convert txt to bool or int if self._lines > 0: - for fieldname in sharing.DB_FIELDS_V1_BOOL: - try: - row[fieldname] = config._convert_to_bool(row[fieldname]) - except Exception as e: - logger.error("sharing database row error fieldname=%r row=%r error: %r", fieldname, row, e) - for fieldname in sharing.DB_FIELDS_V1_INT: - row[fieldname] = int(row[fieldname]) + for fieldname in row: + if logger.isEnabledFor(logging.DEBUG): + logger.debug("TRACE/sharing/_load: test fieldname=%r", fieldname) + if fieldname not in sharing.DB_TYPES_V1: + logger.error("sharing database row error, unsupported fieldname found: %r", fieldname) + return False + if sharing.DB_TYPES_V1[fieldname] is bool: + try: + row[fieldname] = config._convert_to_bool(row[fieldname]) + except Exception as e: + logger.error("sharing database row error in type conversion fieldname=%r row=%r error: %r", fieldname, row, e) + return False + elif sharing.DB_TYPES_V1[fieldname] is int: + try: + row[fieldname] = int(row[fieldname]) + except Exception as e: + logger.error("sharing database row error in type conversion fieldname=%r row=%r error: %r", fieldname, row, e) + return False + elif sharing.DB_TYPES_V1[fieldname] is dict: + if row[fieldname] is None or row[fieldname] == '': + row[fieldname] = {} + else: + field = row[fieldname].lstrip('"').rstrip('"').replace("'", '"') + try: + row[fieldname] = json.loads(field) + except Exception as e: + logger.error("sharing database row error in type conversion fieldname=%r field=%r row=%r error: %r", fieldname, field, row, e) + return False # check for duplicates - dup = False for row_cached in self._sharing_cache: if row == row_cached: - dup = True - break - if dup: - continue + logger.error("sharing database row duplicate row=%r", row) + return False # logger.debug("sharing database load add: %r", row) self._sharing_cache.append(row) self._lines += 1 diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index 8be2575c..889bfa4f 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -77,7 +77,7 @@ class TestSharingApiSanity(BaseTest): response = responses[path] assert not isinstance(response, int) status, prop = response["ICAL:calendar-color"] - logging.debug("calendar-color: %r", prop.text) + logging.debug("ICAL:calendar-color: %r", prop.text) assert status == 200 return prop.text @@ -115,6 +115,56 @@ class TestSharingApiSanity(BaseTest): assert status == 200 and not prop.text return + def _propfind_calendar_description(self, path, login): + _, responses = self.propfind(path=path, data="""\ + + + + + +""", login=login) + logging.info("response: %r", responses) + response = responses[path] + assert not isinstance(response, int) + status, prop = response["C:calendar-description"] + logging.debug("C:calendar-description: %r", prop.text) + assert status == 200 + return prop.text + + def _proppatch_calendar_description(self, path, login, description) -> None: + _, responses = self.proppatch(path=path, data="""\ + + + + + """ + description + """ + + +""", login=login) + logging.info("response: %r", responses) + response = responses[path] + assert not isinstance(response, int) and len(response) == 1 + status, prop = response["C:calendar-description"] + assert status == 200 and not prop.text + return + + def _proppatch_calendar_description_remove(self, path, login) -> None: + _, responses = self.proppatch(path=path, data="""\ + + + + + + + +""", login=login) + logging.info("response: %r", responses) + response = responses[path] + assert not isinstance(response, int) and len(response) == 1 + status, prop = response["C:calendar-description"] + assert status == 200 and not prop.text + return + # Test functions def test_sharing_api_base_csv_custom(self) -> None: self.database_path = os.path.join(self.colpath, "collection-db/test.csv") @@ -3320,6 +3370,16 @@ permissions: RrWw""") color = self._propfind_calendar_color(path_shared_r, login="user:userpw") assert color == "#BBBBBB" + # one property have to be visible + logging.info("\n*** list check for one property (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' in answer_dict['Content'][0]['Properties'] + assert answer_dict['Content'][0]['Properties']['ICAL:calendar-color'] == "#BBBBBB" + # update map by owner logging.info("\n*** update map by owner (disable property overlay)") json_dict = {} @@ -3346,6 +3406,16 @@ permissions: RrWw""") json_dict['Properties'] = {"ICAL:calendar-color": "#CCCCCC"} _, headers, answer = self._sharing_api_json("map", "update", check=403, login="user:userpw", json_dict=json_dict) + # one property have to be visible + logging.info("\n*** list check for one property (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' in answer_dict['Content'][0]['Properties'] + assert answer_dict['Content'][0]['Properties']['ICAL:calendar-color'] == "#BBBBBB" + # verify overlay as user logging.info("\n*** PROPFIND collection user (overlay) -> ok") color = self._propfind_calendar_color(path_shared_r, login="user:userpw") @@ -3360,6 +3430,16 @@ permissions: RrWw""") json_dict['User'] = "user" _, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict) + # one property have to be visible + logging.info("\n*** list check for one property (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' in answer_dict['Content'][0]['Properties'] + assert answer_dict['Content'][0]['Properties']['ICAL:calendar-color'] == "#BBBBBB" + logging.info("\n*** update map by user (json) -> 200 (overlay permitted by share permissions)") json_dict = {} json_dict['PathOrToken'] = path_shared_r @@ -3381,6 +3461,16 @@ permissions: RrWw""") json_dict['Permissions'] = "rp" _, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict) + # one property have to be visible + logging.info("\n*** list check for one property (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' in answer_dict['Content'][0]['Properties'] + assert answer_dict['Content'][0]['Properties']['ICAL:calendar-color'] == "#CCCCCC" + logging.info("\n*** update map by user (json) -> 403 (overlay permitted but denied by share permissions)") json_dict = {} json_dict['PathOrToken'] = path_shared_r @@ -3591,3 +3681,274 @@ permissions: RrWw""") logging.info("\n*** PROPFIND collection owner (visible change) -> ok") color = self._propfind_calendar_color(path_mapped, login="owner:ownerpw") assert color == "#FFFFFF" + + def test_sharing_api_map_propfind_overlay_partial(self) -> None: + """share-by-map API usage tests related to partial overlay.""" + self.configure({"auth": {"type": "htpasswd", + "htpasswd_filename": self.htpasswd_file_path, + "htpasswd_encryption": "plain"}, + "sharing": { + "type": "csv", + "permit_create_map": True, + "permit_create_token": True, + "permit_properties_overlay": "True", + "enforce_properties_overlay": "True", + "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"}}) + + json_dict: dict + + logging.info("\n*** prepare and test access") + + for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)): + logging.info("\n*** test: %s", db_type) + self.configure({"sharing": {"type": db_type}}) + + path_mapped = "/owner/calendarPFP-" + db_type + ".ics/" + path_shared_r = "/user/calendarPFP-shared-by-owner-r-" + db_type + ".ics/" + self.mkcalendar(path_mapped, login="owner:ownerpw") + + # check PROPFIND as owner + logging.info("\n*** PROPFIND collection owner -> ok") + _, responses = self.propfind(path_mapped, """\ + + + + + +""", login="owner:ownerpw") + logging.info("response: %r", responses) + response = responses[path_mapped] + assert not isinstance(response, int) and len(response) == 1 + status, prop = response["D:current-user-principal"] + assert status == 200 and len(prop) == 1 + element = prop.find(xmlutils.make_clark("D:href")) + assert element is not None and element.text == "/owner/" + + # execute PROPPATCH color as owner + logging.info("\n*** PROPPATCH color collection owner -> ok") + self._proppatch_calendar_color(path_mapped, login="owner:ownerpw", color="#AAAAAA") + + # verify PROPPATCH color by owner + logging.info("\n*** PROPFIND color collection owner (verify collection change) -> ok") + color = self._propfind_calendar_color(path_mapped, login="owner:ownerpw") + assert color == "#AAAAAA" + + # execute PROPPATCH description as owner + logging.info("\n*** PROPPATCH description collection owner -> ok") + self._proppatch_calendar_description(path_mapped, login="owner:ownerpw", description="OWNER") + + # verify PROPPATCH description by owner + logging.info("\n*** PROPFIND description collection owner (verify collection change) -> ok") + description = self._propfind_calendar_description(path_mapped, login="owner:ownerpw") + assert description == "OWNER" + + # create map + logging.info("\n*** create map user/owner:rP -> ok") + json_dict = {} + json_dict['User'] = "user" + json_dict['PathMapped'] = path_mapped + json_dict['PathOrToken'] = path_shared_r + json_dict['Permissions'] = "rP" + json_dict['Enabled'] = True + json_dict['Hidden'] = False + _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + + # enable map by user + logging.info("\n*** enable map by user") + json_dict = {} + json_dict['User'] = "user" + json_dict['PathMapped'] = path_mapped + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user:userpw", json_dict=json_dict) + + # verify PROPPATCH as user + logging.info("\n*** PROPFIND color collection collection user -> ok") + color = self._propfind_calendar_color(path_shared_r, login="user:userpw") + assert color == "#AAAAAA" + + logging.info("\n*** PROPFIND description collection collection user -> ok") + description = self._propfind_calendar_description(path_shared_r, login="user:userpw") + assert description == "OWNER" + + # execute PROPPATCH color as user + logging.info("\n*** PROPPATCH color collection user -> ok") + self._proppatch_calendar_color(path_shared_r, login="user:userpw", color="#BBBBBB") + + # one property has to be visible + logging.info("\n*** list (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' in answer_dict['Content'][0]['Properties'] + assert answer_dict['Content'][0]['Properties']['ICAL:calendar-color'] == "#BBBBBB" + + # execute PROPPATCH description as user + logging.info("\n*** PROPPATCH description collection user -> ok") + self._proppatch_calendar_description(path_shared_r, login="user:userpw", description="USER") + + # both properties have to be visible + logging.info("\n*** list check for both properties (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' in answer_dict['Content'][0]['Properties'] + assert answer_dict['Content'][0]['Properties']['ICAL:calendar-color'] == "#BBBBBB" + assert 'C:calendar-description' in answer_dict['Content'][0]['Properties'] + assert answer_dict['Content'][0]['Properties']['C:calendar-description'] == "USER" + + # verify PROPPATCH as user + logging.info("\n*** PROPFIND color collection collection user -> ok") + color = self._propfind_calendar_color(path_shared_r, login="user:userpw") + assert color == "#BBBBBB" + + logging.info("\n*** PROPFIND description collection collection user -> ok") + description = self._propfind_calendar_description(path_shared_r, login="user:userpw") + assert description == "USER" + + # execute PROPPATCH DELETE description as user + logging.info("\n*** PROPPATCH DELETE description collection user -> ok") + self._proppatch_calendar_description_remove(path_shared_r, login="user:userpw") + + # one property has to survive + logging.info("\n*** list check for still one property (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' in answer_dict['Content'][0]['Properties'] + assert answer_dict['Content'][0]['Properties']['ICAL:calendar-color'] == "#BBBBBB" + + # set properties by user using API + logging.info("\n*** set properties by user color overwrite (form)") + form_array = [] + form_array.append("PathOrToken=" + path_shared_r) + form_array.append("Properties='ICAL:calendar-color'='#CCCCCC'") + _, headers, answer = self._sharing_api_form("map", "update", check=200, login="user:userpw", form_array=form_array) + + # one property has to survive + logging.info("\n*** list check for still one property (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' in answer_dict['Content'][0]['Properties'] + assert answer_dict['Content'][0]['Properties']['ICAL:calendar-color'] == "#CCCCCC" + assert 'C:calendar-description' not in answer_dict['Content'][0]['Properties'] + + # set property by user using API + logging.info("\n*** set properties by user description extension (form)") + form_array = [] + form_array.append("PathOrToken=" + path_shared_r) + form_array.append("Properties='C:calendar-description'='USER-OWNER'") + _, headers, answer = self._sharing_api_form("map", "update", check=200, login="user:userpw", form_array=form_array) + + # both properties have to be visible + logging.info("\n*** list check for both properties (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' in answer_dict['Content'][0]['Properties'] + assert answer_dict['Content'][0]['Properties']['ICAL:calendar-color'] == "#CCCCCC" + assert 'C:calendar-description' in answer_dict['Content'][0]['Properties'] + assert answer_dict['Content'][0]['Properties']['C:calendar-description'] == "USER-OWNER" + + # delete property by user using API + logging.info("\n*** delete property by user color (form)") + form_array = [] + form_array.append("PathOrToken=" + path_shared_r) + form_array.append("Properties='ICAL:calendar-color'=''") + _, headers, answer = self._sharing_api_form("map", "update", check=200, login="user:userpw", form_array=form_array) + + # only one property has to be visible + logging.info("\n*** list check for single properties (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' not in answer_dict['Content'][0]['Properties'] + assert 'C:calendar-description' in answer_dict['Content'][0]['Properties'] + assert answer_dict['Content'][0]['Properties']['C:calendar-description'] == "USER-OWNER" + + # clear all propertie by user using API + logging.info("\n*** delete property by user color (form)") + form_array = [] + form_array.append("PathOrToken=" + path_shared_r) + form_array.append("Properties=") + _, headers, answer = self._sharing_api_form("map", "update", check=200, login="user:userpw", form_array=form_array) + + # no property has to be visible + logging.info("\n*** list check empty properties (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' not in answer_dict['Content'][0]['Properties'] + assert 'C:calendar-description' not in answer_dict['Content'][0]['Properties'] + + # set properties by user using API + logging.info("\n*** set properties by user (json)") + json_dict = {} + json_dict["PathOrToken"] = path_shared_r + json_dict["Properties"] = {'C:calendar-description': 'USER-OWNER', 'ICAL:calendar-color': '#DDDDDD'} + _, headers, answer = self._sharing_api_json("map", "update", check=200, login="user:userpw", json_dict=json_dict) + + # both properties have to be visible + logging.info("\n*** list check for both properties (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' in answer_dict['Content'][0]['Properties'] + assert 'C:calendar-description' in answer_dict['Content'][0]['Properties'] + + # delete on property by user using API + logging.info("\n*** delete property by user color (json)") + json_dict = {} + json_dict["PathOrToken"] = path_shared_r + json_dict["Properties"] = {'C:calendar-description': ''} + _, headers, answer = self._sharing_api_json("map", "update", check=200, login="user:userpw", json_dict=json_dict) + + # one property have to be visible + logging.info("\n*** list check for one property (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' in answer_dict['Content'][0]['Properties'] + assert 'C:calendar-description' not in answer_dict['Content'][0]['Properties'] + + # delete all propertie by user using API + logging.info("\n*** delete all properties by user (json)") + json_dict = {} + json_dict["PathOrToken"] = path_shared_r + json_dict["Properties"] = {} + _, headers, answer = self._sharing_api_json("map", "update", check=200, login="user:userpw", json_dict=json_dict) + + # no property has to be visible + logging.info("\n*** list check empty properties (json->json)") + json_dict['PathOrToken'] = path_shared_r + _, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict) + answer_dict = json.loads(answer) + assert answer_dict['Status'] == "success" + assert answer_dict['Lines'] == 1 + assert 'ICAL:calendar-color' not in answer_dict['Content'][0]['Properties'] + assert 'C:calendar-description' not in answer_dict['Content'][0]['Properties']