diff --git a/tests/storage/dav/__init__.py b/tests/storage/dav/__init__.py index 2ff2fcf7..f43abe64 100644 --- a/tests/storage/dav/__init__.py +++ b/tests/storage/dav/__init__.py @@ -1,11 +1,14 @@ from __future__ import annotations import os +import re import uuid +import xml.etree.ElementTree as ET import aiohttp import aiostream import pytest +from aioresponses import aioresponses from tests import assert_item_equals from tests.storage import StorageTests @@ -51,3 +54,50 @@ async def test_dav_unicode_href(self, s, get_item, monkeypatch): href, _etag = await s.upload(item) item2, _etag2 = await s.get(href) assert_item_equals(item, item2) + + @pytest.mark.asyncio + async def test_dav_get_multi_missing_href_batch_is_nonfatal( + self, s, get_item, monkeypatch + ): + item = get_item() + existing_href, _etag = await s.upload(item) + missing_href = existing_href + ".missing" + + def _fake_parse_prop_responses(_root): + prop = ET.Element("prop") + ET.SubElement(prop, s.get_multi_data_query).text = item.raw + return [(existing_href, '"etag-existing"', prop)] + + monkeypatch.setattr(s, "_parse_prop_responses", _fake_parse_prop_responses) + url = str(s.url).rstrip("/") + url_pattern = re.compile(rf"^{re.escape(url)}/?$") + with aioresponses() as m: + m.add(url_pattern, method="REPORT", status=207, body="") + result = await aiostream.stream.list( + s.get_multi([existing_href, missing_href]) + ) + assert len(m.requests) == 1 + assert len(result) == 1 + href, returned_item, etag = result[0] + assert href == existing_href + assert etag == '"etag-existing"' + assert_item_equals(item, returned_item) + + @pytest.mark.asyncio + async def test_dav_get_multi_missing_single_href_raises( + self, s, get_item, monkeypatch + ): + existing_href, _etag = await s.upload(get_item()) + href = existing_href + ".missing" + + def _fake_parse_prop_responses(_root): + return [] + + monkeypatch.setattr(s, "_parse_prop_responses", _fake_parse_prop_responses) + url = str(s.url).rstrip("/") + url_pattern = re.compile(rf"^{re.escape(url)}/?$") + with aioresponses() as m: + m.add(url_pattern, method="REPORT", status=207, body="") + with pytest.raises(exceptions.NotFoundError): + await aiostream.stream.list(s.get_multi([href])) + assert len(m.requests) == 1 diff --git a/vdirsyncer/storage/dav.py b/vdirsyncer/storage/dav.py index fcf5ae07..d959e36a 100644 --- a/vdirsyncer/storage/dav.py +++ b/vdirsyncer/storage/dav.py @@ -549,9 +549,17 @@ async def get_multi(self, hrefs): dav_logger.warning(f"Server sent unsolicited item: {href}") else: rv.append((href, Item(raw), etag)) - for href in hrefs_left: + if len(hrefs) == 1 and hrefs_left: + # Preserve get(href) semantics for single-item lookups. + (href,) = hrefs_left raise exceptions.NotFoundError(href) + # In multiget, tolerate transiently missing hrefs from the server. + for href in hrefs_left: + dav_logger.warning( + f"Skipping {href}, server did not return the item in REPORT." + ) + for href, item, etag in rv: yield href, item, etag