# This file is part of Radicale - CalDAV and CardDAV server # Copyright © 2012-2017 Guillaume Ayoub # Copyright © 2017-2022 Unrud # Copyright © 2024-2026 Peter Bieringer # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Radicale. If not, see . """ Tests for storage backends. """ import json import logging import os import re import shutil import tempfile from typing import ClassVar, cast import pytest import radicale.tests.custom.storage_simple_sync from radicale import logger, pathutils from radicale.tests import BaseTest from radicale.tests.helpers import get_file_content from radicale.tests.test_base import TestBaseRequests as _TestBaseRequests class TestMultiFileSystem(BaseTest): """Tests for multifilesystem.""" def setup_method(self) -> None: _TestBaseRequests.setup_method(cast(_TestBaseRequests, self)) self.configure({"storage": {"type": "multifilesystem"}}) def test_folder_creation(self) -> None: """Verify that the folder is created.""" folder = os.path.join(self.colpath, "subfolder") self.configure({"storage": {"filesystem_folder": folder}}) assert os.path.isdir(folder) def test_folder_creation_with_umask(self) -> None: """Verify that the folder is created with umask.""" folder = os.path.join(self.colpath, "subfolder") self.configure({"storage": {"filesystem_folder": folder, "folder_umask": "0077"}}) assert os.path.isdir(folder) def test_fsync(self) -> None: """Create a directory and file with syncing enabled.""" self.configure({"storage": {"_filesystem_fsync": "True"}}) self.mkcalendar("/calendar.ics/") def test_hook(self) -> None: """Run hook.""" self.configure({"storage": {"hook": "mkdir %s" % os.path.join( "collection-root", "created_by_hook")}}) self.mkcalendar("/calendar.ics/") self.propfind("/created_by_hook/") def test_hook_read_access(self) -> None: """Verify that hook is not run for read accesses.""" self.configure({"storage": {"hook": "mkdir %s" % os.path.join( "collection-root", "created_by_hook")}}) self.propfind("/") self.propfind("/created_by_hook/", check=404) @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") def test_hook_storage_locked(self) -> None: """Verify that the storage is locked when the hook runs.""" self.configure({"storage": {"hook": ( "flock -n .Radicale.lock || exit 0; exit 1")}}) self.mkcalendar("/calendar.ics/") def test_hook_principal_collection_creation(self) -> None: """Verify that the hooks runs when a new user is created.""" self.configure({"storage": {"hook": "mkdir %s" % os.path.join( "collection-root", "created_by_hook")}}) self.configure({"auth": {"type": "none"}}) self.propfind("/", login="user:") self.propfind("/created_by_hook/") def test_hook_fail(self) -> None: """Verify that a request succeeded if the hook still fails (anyhow no rollback implemented).""" self.configure({"storage": {"hook": "exit 1"}}) self.mkcalendar("/calendar.ics/", check=201) def test_item_cache_rebuild(self) -> None: """Delete the item cache and verify that it is rebuild.""" self.mkcalendar("/calendar.ics/") event = get_file_content("event1.ics") path = "/calendar.ics/event1.ics" self.put(path, event) _, answer1 = self.get(path) cache_folder = os.path.join(self.colpath, "collection-root", "calendar.ics", ".Radicale.cache", "item") assert os.path.exists(os.path.join(cache_folder, "event1.ics")) shutil.rmtree(cache_folder) _, answer2 = self.get(path) assert answer1 == answer2 assert os.path.exists(os.path.join(cache_folder, "event1.ics")) def test_item_cache_rebuild_subfolder(self) -> None: """Delete the item cache and verify that it is rebuild.""" self.configure({"storage": {"use_cache_subfolder_for_item": "True"}}) self.mkcalendar("/calendar.ics/") event = get_file_content("event1.ics") path = "/calendar.ics/event1.ics" self.put(path, event) _, answer1 = self.get(path) cache_folder = os.path.join(self.colpath, "collection-cache", "calendar.ics", ".Radicale.cache", "item") assert os.path.exists(os.path.join(cache_folder, "event1.ics")) shutil.rmtree(cache_folder) _, answer2 = self.get(path) assert answer1 == answer2 assert os.path.exists(os.path.join(cache_folder, "event1.ics")) def test_item_cache_rebuild_mtime_and_size(self) -> None: """Delete the item cache and verify that it is rebuild.""" self.configure({"storage": {"use_mtime_and_size_for_item_cache": "True"}}) self.mkcalendar("/calendar.ics/") event = get_file_content("event1.ics") path = "/calendar.ics/event1.ics" self.put(path, event) _, answer1 = self.get(path) cache_folder = os.path.join(self.colpath, "collection-root", "calendar.ics", ".Radicale.cache", "item") assert os.path.exists(os.path.join(cache_folder, "event1.ics")) shutil.rmtree(cache_folder) _, answer2 = self.get(path) assert answer1 == answer2 assert os.path.exists(os.path.join(cache_folder, "event1.ics")) def test_put_items_multiple(self) -> None: """Upload 2 items to calendar, check that collection inode number stays.""" self.configure({"logging": {"response_content_on_debug": "False", "response_header_on_debug": "False", "request_content_on_debug": "False", "request_header_on_debug": "False", }}) self.mkcalendar("/calendar.ics/") event1 = get_file_content("event1.ics") path1 = "/calendar.ics/event1.ics" event2 = get_file_content("event2.ics") path2 = "/calendar.ics/event2.ics" self.put(path1, event1) collection_folder = os.path.join(self.colpath, "collection-root", "calendar.ics") collection_folder_st_ino_1 = os.stat(collection_folder).st_ino logger.debug("path=%r stat.ST_INO=%d", collection_folder, collection_folder_st_ino_1) self.put(path2, event2) collection_folder_st_ino_2 = os.stat(collection_folder).st_ino logger.debug("path=%r stat.ST_INO=%d", collection_folder, collection_folder_st_ino_2) assert collection_folder_st_ino_1 == collection_folder_st_ino_2 def test_put_calendar_multiple(self) -> None: """Upload 2 calendars, check that collection inode number changes.""" self.configure({"logging": {"response_content_on_debug": "False", "response_header_on_debug": "False", "request_content_on_debug": "False", "request_header_on_debug": "False", }}) self.put("/calendar.ics/", "BEGIN:VCALENDAR\r\nEND:VCALENDAR") collection_folder = os.path.join(self.colpath, "collection-root", "calendar.ics") collection_folder_st_ino_1 = os.stat(collection_folder).st_ino logger.debug("path=%r stat.ST_INO=%d", collection_folder, collection_folder_st_ino_1) # Overwrite events = get_file_content("event_multiple.ics") self.put("/calendar.ics/", events) collection_folder_st_ino_2 = os.stat(collection_folder).st_ino logger.debug("path=%r stat.ST_INO=%d", collection_folder, collection_folder_st_ino_2) assert collection_folder_st_ino_1 != collection_folder_st_ino_2 def test_put_whole_calendar_uids_used_as_file_names(self) -> None: """Test if UIDs are used as file names.""" _TestBaseRequests.test_put_whole_calendar( cast(_TestBaseRequests, self)) for uid in ("todo", "event"): _, answer = self.get("/calendar.ics/%s.ics" % uid) assert "\r\nUID:%s\r\n" % uid in answer def test_put_whole_calendar_random_uids_used_as_file_names(self) -> None: """Test if UIDs are used as file names.""" _TestBaseRequests.test_put_whole_calendar_without_uids( cast(_TestBaseRequests, self)) _, answer = self.get("/calendar.ics") assert answer is not None uids = [] for line in answer.split("\r\n"): if line.startswith("UID:"): uids.append(line[len("UID:"):]) for uid in uids: _, answer = self.get("/calendar.ics/%s.ics" % uid) assert answer is not None assert "\r\nUID:%s\r\n" % uid in answer def test_put_whole_addressbook_uids_used_as_file_names(self) -> None: """Test if UIDs are used as file names.""" _TestBaseRequests.test_put_whole_addressbook( cast(_TestBaseRequests, self)) for uid in ("contact1", "contact2"): _, answer = self.get("/contacts.vcf/%s.vcf" % uid) assert "\r\nUID:%s\r\n" % uid in answer def test_put_whole_addressbook_random_uids_used_as_file_names( self) -> None: """Test if UIDs are used as file names.""" _TestBaseRequests.test_put_whole_addressbook_without_uids( cast(_TestBaseRequests, self)) _, answer = self.get("/contacts.vcf") assert answer is not None uids = [] for line in answer.split("\r\n"): if line.startswith("UID:"): uids.append(line[len("UID:"):]) for uid in uids: _, answer = self.get("/contacts.vcf/%s.vcf" % uid) assert answer is not None assert "\r\nUID:%s\r\n" % uid in answer @pytest.mark.skipif(not pathutils.path_is_collision_free_case_sensitive(tempfile.mkdtemp()), reason="TEMP is not case sensitive") def test_collection_storage_dummy_case_sensitivity(self) -> None: """Test collection storage case sensitivity.""" @pytest.mark.skipif(not pathutils.path_is_collision_free_no_short_filename(tempfile.mkdtemp()), reason="TEMP has short filename") def test_collection_storage_dummy_no_short_filename(self) -> None: """Test collection storage no short filename.""" @pytest.mark.skipif(not pathutils.path_supports_unicode(tempfile.mkdtemp()), reason="TEMP is not supporting unicode") def test_collection_storage_dummy_no_support_of_unicode(self) -> None: """Test collection storage no support of unicode.""" @pytest.mark.skipif(not pathutils.path_supports_trailing_whitespace(tempfile.mkdtemp()), reason="TEMP is not supporting trailing whitespace") def test_collection_storage_dummy_no_support_of_trailing_whitespace(self) -> None: """Test collection storage no support of trailing space.""" @pytest.mark.skipif(not pathutils.path_supports_problematic_chars(tempfile.mkdtemp()), reason="TEMP is not supporting problematic chars") def test_collection_storage_dummy_no_support_of_problematic_chars(self) -> None: """Test collection storage no support of problematic chars.""" @pytest.mark.skipif(not pathutils.path_supports_symlink(tempfile.mkdtemp()), reason="TEMP is not supporting symlink") def test_collection_sharing_by_softlink(self) -> None: """Test collection sharing by softlink.""" self.configure({"auth": {"type": "none"}}) path_group_col = "/group/calendar-shared.ics/" file_item = "event1.ics" self.mkcalendar(path_group_col, login="group:grouppw") event = get_file_content(file_item) self.put(path_group_col + file_item, event) fs_path_group_col = os.path.join(self.colpath, "collection-root", "group", "calendar-shared.ics") fs_path_group_col_rel = os.path.join("..", "group", "calendar-shared.ics") fs_path_user = os.path.join(self.colpath, "collection-root", "user") fs_path_user_col = os.path.join(self.colpath, "collection-root", "user", "calendar-group.ics") logger.debug("colpath=%r fs_path_group_col=%r", self.colpath, fs_path_group_col) assert os.path.isdir(fs_path_group_col) # create user directory and check logger.debug("create dir fs_path_user=%r", fs_path_user) os.mkdir(fs_path_user) assert os.path.isdir(fs_path_user) # create relative symlink and check logger.debug("create symlink src=%r dst=%r", fs_path_group_col_rel, fs_path_user_col) os.symlink(fs_path_group_col_rel, fs_path_user_col) logger.debug("test exists: fs_path_user_col=%r", fs_path_user_col) assert os.path.exists(fs_path_user_col) logger.debug("test is softlink: fs_path_user_col=%r", fs_path_user_col) assert os.path.islink(fs_path_user_col) self.propfind("/user/", login="user:userpw", HTTP_DEPTH="1") def test_colliding_items_by_file_case_insensitive(self, caplog) -> None: """Test for colliding files on file systems.""" caplog.set_level(logging.WARNING) self.configure({"logging": {"request_content_on_debug": "False"}}) fs_collision_free = pathutils.path_is_collision_free_case_sensitive(self.colpath) file_item = "EvEnT1.iCs" path_coll = "/calendar.ics/" self.mkcalendar(path_coll) event = get_file_content(file_item.lower()) path_item = os.path.join(path_coll, file_item) path_uc = os.path.join(path_coll, file_item.upper()) path_lc = os.path.join(path_coll, file_item.lower()) self.put(path_item, event) self.put(path_uc, event, check=409) if not fs_collision_free: logs = caplog.messages assert len([log for log in logs if "File name collision" in log]) == 1 self.put(path_lc, event, check=409) if not fs_collision_free: logs = caplog.messages assert len([log for log in logs if "File name collision" in log]) == 2 def test_colliding_items_by_dir_case_insensitive(self, caplog) -> None: """Test for colliding dirs on file systems.""" caplog.set_level(logging.WARNING) self.configure({"logging": {"request_content_on_debug": "False"}}) fs_collision_free = pathutils.path_is_collision_free_case_sensitive(self.colpath) path_coll = "/CaLeNdAr.ics/" self.mkcalendar(path_coll) if fs_collision_free: self.mkcalendar(path_coll.lower(), check=201) else: self.mkcalendar(path_coll.lower(), check=409) logs = caplog.messages assert len([log for log in logs if "File name collision" in log]) == 1 if fs_collision_free: self.mkcalendar(path_coll.upper(), check=201) else: self.mkcalendar(path_coll.upper(), check=409) logs = caplog.messages assert len([log for log in logs if "File name collision" in log]) == 2 def test_colliding_items_by_dir_shortname(self, caplog) -> None: """Test for colliding dirs (shortname) on file systems.""" caplog.set_level(logging.WARNING) self.configure({"logging": {"request_content_on_debug": "False"}}) fs_collision_free = pathutils.path_is_collision_free_no_short_filename(self.colpath) fs_collision_free_case_sensitive = pathutils.path_is_collision_free_case_sensitive(self.colpath) path_coll = "/calendarlongname.ics/" path_coll_short = "/calend~1.ics/" self.mkcalendar(path_coll) if fs_collision_free: self.mkcalendar(path_coll_short.lower(), check=201) if not fs_collision_free_case_sensitive: # cleanup to avoid collision below self.delete(path_coll_short.lower()) else: self.mkcalendar(path_coll_short.lower(), check=409) logs = caplog.messages assert len([log for log in logs if "File name collision" in log]) == 1 if fs_collision_free: self.mkcalendar(path_coll_short.upper(), check=201) else: self.mkcalendar(path_coll_short.upper(), check=409) logs = caplog.messages assert len([log for log in logs if "File name collision" in log]) == 2 def test_propfind_depth1_does_not_check_collisions_per_item(self) -> None: """Listing a collection (PROPFIND depth:1) must not re-run the filesystem collision check (path_to_filesystem) once per item. """ self.mkcalendar("/calendar.ics/") item_count = 20 for index in range(item_count): uid = "event%d" % index event = ("BEGIN:VCALENDAR\r\nVERSION:2.0\r\n" "BEGIN:VEVENT\r\nUID:%s\r\nSUMMARY:Event %d\r\n" "DTSTART:20130901T180000Z\r\nEND:VEVENT\r\n" "END:VCALENDAR\r\n" % (uid, index)) self.put("/calendar.ics/%s.ics" % uid, event) call_count = 0 original_path_to_filesystem = pathutils.path_to_filesystem def counting_path_to_filesystem(*args, **kwargs): nonlocal call_count call_count += 1 return original_path_to_filesystem(*args, **kwargs) with pytest.MonkeyPatch.context() as monkeypatch: monkeypatch.setattr(pathutils, "path_to_filesystem", counting_path_to_filesystem) _, responses = self.propfind("/calendar.ics/", """\ """, HTTP_DEPTH="1") assert len(responses) == item_count + 1 assert call_count < item_count @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") @pytest.mark.skipif(radicale.log.logger.getEffectiveLevel() == logging.INFO, reason="requires loglevel DEBUG") def test_hook_placeholders_PUT(self, caplog) -> None: """Run hook and check placeholders via debug log: PUT""" caplog.set_level(logging.DEBUG) self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) found = 0 self.mkcalendar("/calendar.ics/") event = get_file_content("event1.ics") path = "/calendar.ics/event1.ics" self.put(path, event) for line in caplog.messages: if line.find("\"hook-json ") != -1: found = 1 r = re.search('.*\"hook-json ({.*})".*', line) if r: s = r.group(1).replace("'", "\"") else: break d = json.loads(s) if d["user"] == "Anonymous": found = found | 2 if d["cwd"]: found = found | 4 if d["path"]: found = found | 8 if d["path"] == d["cwd"] + "/collection-root/calendar.ics/event1.ics": found = found | 16 if d["request"]: found = found | 64 if d["request"] == "PUT": found = found | 128 if d["to_path"]: found = found | 32 if d["to_path"] == "": found = found | 256 else: found = found | 256 | 32 if (found != 511): raise ValueError("Logging misses expected hook log line, found=%d data=%r", found, d) else: logging.info("Logging contains expected hook line, found=%d data=%r", found, d) @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") @pytest.mark.skipif(radicale.log.logger.getEffectiveLevel() == logging.INFO, reason="requires loglevel DEBUG") def test_hook_placeholders_DELETE(self, caplog) -> None: """Run hook and check placeholders via debug log: DELETE""" caplog.set_level(logging.DEBUG) self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) found = 0 self.mkcalendar("/calendar.ics/") event = get_file_content("event1.ics") path = "/calendar.ics/event1.ics" self.put(path, event) self.delete(path) for line in caplog.messages: if line.find("\"hook-json ") != -1: found = 1 r = re.search('.*\"hook-json ({.*})".*', line) if r: s = r.group(1).replace("'", "\"") else: break d = json.loads(s) if d["user"] == "Anonymous": found = found | 2 if d["cwd"]: found = found | 4 if d["path"]: found = found | 8 if d["path"] == d["cwd"] + "/collection-root/calendar.ics/event1.ics": found = found | 16 if d["request"]: found = found | 64 if d["request"] == "DELETE": found = found | 128 if d["to_path"]: found = found | 32 if d["to_path"] == "": found = found | 256 else: found = found | 256 | 32 if (found != 511): raise ValueError("Logging misses expected hook log line, found=%d data=%r", found, s) else: logging.info("Logging contains expected hook line, found=%d data=%r", found, d) @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") @pytest.mark.skipif(radicale.log.logger.getEffectiveLevel() == logging.INFO, reason="requires loglevel DEBUG") def test_hook_placeholders_MKCALENDAR(self, caplog) -> None: """Run hook and check placeholders via debug log: MKCALENDAR""" caplog.set_level(logging.DEBUG) self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) found = 0 self.mkcalendar("/calendar.ics/") for line in caplog.messages: if line.find("\"hook-json ") != -1: found = 1 r = re.search('.*\"hook-json ({.*})".*', line) if r: s = r.group(1).replace("'", "\"") else: break d = json.loads(s) if d["user"] == "Anonymous": found = found | 2 if d["cwd"]: found = found | 4 if d["path"]: found = found | 8 if d["path"] == d["cwd"] + "/collection-root/calendar.ics/": found = found | 16 if d["request"]: found = found | 64 if d["request"] == "MKCALENDAR": found = found | 128 if d["to_path"]: found = found | 32 if d["to_path"] == "": found = found | 256 else: found = found | 256 | 32 if (found != 511): raise ValueError("Logging misses expected hook log line, found=%d data=%r", found, d) else: logging.info("Logging contains expected hook line, found=%d data=%r", found, d) @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") @pytest.mark.skipif(radicale.log.logger.getEffectiveLevel() == logging.INFO, reason="requires loglevel DEBUG") def test_hook_placeholders_MKCOL(self, caplog) -> None: """Run hook and check placeholders via debug log: MKCOL""" caplog.set_level(logging.DEBUG) self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) found = 0 self.mkcol("/user1/") for line in caplog.messages: if line.find("\"hook-json ") != -1: found = 1 r = re.search('.*\"hook-json ({.*})".*', line) if r: s = r.group(1).replace("'", "\"") else: break d = json.loads(s) if d["user"] == "Anonymous": found = found | 2 if d["cwd"]: found = found | 4 if d["path"]: found = found | 8 if d["path"] == d["cwd"] + "/collection-root/user1/": found = found | 16 if d["request"]: found = found | 64 if d["request"] == "MKCOL": found = found | 128 if d["to_path"]: found = found | 32 if d["to_path"] == "": found = found | 256 else: found = found | 256 | 32 if (found != 511): raise ValueError("Logging misses expected hook log line, found=%d data=%r", found, d) else: logging.info("Logging contains expected hook line, found=%d data=%r", found, d) @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") @pytest.mark.skipif(radicale.log.logger.getEffectiveLevel() == logging.INFO, reason="requires loglevel DEBUG") def test_hook_placeholders_PROPPATCH(self, caplog) -> None: """Run hook and check placeholders via debug log: PROPPATCH""" caplog.set_level(logging.DEBUG) self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) found = 0 self.mkcalendar("/calendar.ics/") proppatch = get_file_content("proppatch_set_calendar_color.xml") _, responses = self.proppatch("/calendar.ics/", proppatch) for line in caplog.messages: if line.find("\"hook-json ") != -1: found = 1 r = re.search('.*\"hook-json ({.*})".*', line) if r: s = r.group(1).replace("'", "\"") else: break d = json.loads(s) if d["user"] == "Anonymous": found = found | 2 if d["cwd"]: found = found | 4 if d["path"]: found = found | 8 if d["path"] == d["cwd"] + "/collection-root/calendar.ics/": found = found | 16 if d["request"]: found = found | 64 if d["request"] == "PROPPATCH": found = found | 128 if d["to_path"]: found = found | 32 if d["to_path"] == "": found = found | 256 else: found = found | 256 | 32 if (found != 511): raise ValueError("Logging misses expected hook log line, found=%d data=%r", found, d) else: logging.info("Logging contains expected hook line, found=%d data=%r", found, d) @pytest.mark.skipif(not shutil.which("flock"), reason="flock command not found") @pytest.mark.skipif(radicale.log.logger.getEffectiveLevel() == logging.INFO, reason="requires loglevel DEBUG") def test_hook_placeholders_MOVE(self, caplog) -> None: """Run hook and check placeholders via debug log: MOVE""" caplog.set_level(logging.DEBUG) self.configure({"storage": {"hook": "echo \"hook-json {'user':'%(user)s', 'cwd':'%(cwd)s', 'path':'%(path)s', 'request':'%(request)s', 'to_path':'%(to_path)s'}\""}}) found = 0 self.mkcalendar("/calendar.ics/") event = get_file_content("event1.ics") path1 = "/calendar.ics/event1.ics" path2 = "/calendar.ics/event2.ics" self.put(path1, event) self.request("MOVE", path1, check=201, HTTP_DESTINATION="http://127.0.0.1/"+path2) for line in caplog.messages: if line.find("\"hook-json ") != -1: found = 1 r = re.search('.*\"hook-json ({.*})".*', line) if r: s = r.group(1).replace("'", "\"") else: break d = json.loads(s) if d["user"] == "Anonymous": found = found | 2 if d["cwd"]: found = found | 4 if d["path"]: found = found | 8 if d["path"] == d["cwd"] + "/collection-root/calendar.ics/event1.ics": found = found | 16 if d["request"]: found = found | 64 if d["request"] == "MOVE": found = found | 128 if d["to_path"]: found = found | 32 if d["to_path"] == d["cwd"] + "/collection-root/calendar.ics/event2.ics": found = found | 256 if (found != 511): raise ValueError("Logging misses expected hook log line, found=%d data=%r", found, d) else: logging.info("Logging contains expected hook line, found=%d data=%r", found, d) class TestMultiFileSystemNoLock(BaseTest): """Tests for multifilesystem_nolock.""" def setup_method(self) -> None: _TestBaseRequests.setup_method(cast(_TestBaseRequests, self)) self.configure({"storage": {"type": "multifilesystem_nolock"}}) test_add_event = _TestBaseRequests.test_add_event test_item_cache_rebuild = TestMultiFileSystem.test_item_cache_rebuild class TestCustomStorageSystem(BaseTest): """Test custom backend loading.""" def setup_method(self) -> None: _TestBaseRequests.setup_method(cast(_TestBaseRequests, self)) self.configure({"storage": { "type": "radicale.tests.custom.storage_simple_sync"}}) full_sync_token_support: ClassVar[bool] = False test_add_event = _TestBaseRequests.test_add_event _report_sync_token = _TestBaseRequests._report_sync_token # include tests related to sync token s: str = "" for s in dir(_TestBaseRequests): if s.startswith("test_") and "sync" in s.split("_"): locals()[s] = getattr(_TestBaseRequests, s) del s class TestCustomStorageSystemCallable(BaseTest): """Test custom backend loading with ``callable``.""" def setup_method(self) -> None: _TestBaseRequests.setup_method(cast(_TestBaseRequests, self)) self.configure({"storage": { "type": radicale.tests.custom.storage_simple_sync.Storage}}) test_add_event = _TestBaseRequests.test_add_event