feat(dav,obsidian): grupo dav completo (CardDAV/CalDAV client + split vcf/ics + import pipelines) + build_obsidian_graph + dav_list_calendars

Funciones reutilizables creadas esta sesion para el sistema self-hosted de contactos/calendario (Xandikos) y la app osint_web:
- grupo dav (infra): split_vcards, split_vevents_to_vcalendars, extract_or_make_uid, carddav_put_vcard, caldav_put_event, dav_list_resources, dav_get_resource, dav_list_calendars
- pipelines: import_vcf_to_carddav, import_ics_to_caldav
- obsidian: build_obsidian_graph (grafo agregado del vault)
This commit is contained in:
2026-06-12 00:43:59 +02:00
parent 4a0f0e9dc0
commit a76760edba
32 changed files with 2814 additions and 0 deletions
@@ -0,0 +1,88 @@
"""Tests para dav_get_resource.
Smoke deterministas: monkeypatchean urllib.request.urlopen para capturar el
Request (method GET, auth, URL) y devolver un cuerpo simulado.
"""
import base64
import sys
import infra.dav_get_resource # noqa: F401
mod = sys.modules["infra.dav_get_resource"]
dav_get_resource = mod.dav_get_resource
_BODY = "BEGIN:VCARD\r\nVERSION:3.0\r\nFN:Ada Lovelace\r\nEND:VCARD\r\n"
class _FakeResp:
status = 200
def __enter__(self):
return self
def __exit__(self, *a):
return False
def read(self):
return _BODY.encode("utf-8")
def _capture(monkeypatch):
captured = {}
def fake_urlopen(req, timeout=None, context=None):
captured["url"] = req.full_url
captured["method"] = req.get_method()
captured["headers"] = {k.lower(): v for k, v in req.header_items()}
return _FakeResp()
monkeypatch.setattr(mod.urllib.request, "urlopen", fake_urlopen)
return captured
def test_construye_request_get_con_auth(monkeypatch):
cap = _capture(monkeypatch)
res = dav_get_resource(
"https://dav.example.com", "enmanuel", "secret-pw",
"/enmanuel/contacts/addressbook/ada.vcf",
)
assert res["status"] == "ok"
assert cap["method"] == "GET"
expected = "Basic " + base64.b64encode(b"enmanuel:secret-pw").decode("ascii")
assert cap["headers"]["authorization"] == expected
def test_resource_path_relativo_se_resuelve_con_base_url(monkeypatch):
cap = _capture(monkeypatch)
dav_get_resource(
"https://dav.example.com", "u", "p",
"/enmanuel/contacts/addressbook/ada.vcf",
)
assert cap["url"] == "https://dav.example.com/enmanuel/contacts/addressbook/ada.vcf"
def test_resource_path_absoluto_se_respeta(monkeypatch):
cap = _capture(monkeypatch)
abs_url = "https://otra.example.com/path/x.vcf"
dav_get_resource("https://dav.example.com", "u", "p", abs_url)
assert cap["url"] == abs_url
def test_devuelve_texto_del_recurso(monkeypatch):
_capture(monkeypatch)
res = dav_get_resource(
"https://dav.example.com", "u", "p", "/x.vcf",
)
assert res["text"] == _BODY
assert res["http_status"] == 200
def test_httperror_devuelve_status_error(monkeypatch):
def fake_urlopen(req, timeout=None, context=None):
raise mod.urllib.error.HTTPError(req.full_url, 404, "Not Found", {}, None)
monkeypatch.setattr(mod.urllib.request, "urlopen", fake_urlopen)
res = dav_get_resource("https://dav.example.com", "u", "p", "/x.vcf")
assert res["status"] == "error"
assert res["http_status"] == 404