Bläddra i källkod

Add Radicale CalDAV fixture; run user_cal tests locally

Add radicale to dev dependencies. conftest.py gains two fixtures:

- radicale_server (session): launches a Radicale process on a random port
  with auth.type=none, creates the 'House' calendar, tears down after session.
- require_user_cal (function): previously skipped tests; now redirects
  user_cal to the local Radicale server by monkeypatching get_config in
  room_cal.user_cal and room_cal.actions.fetch_user_cal.

test_user_cal.py (3 tests) and two test_action_chains.py tests that use
fetch_user_cal / diff now run locally without a real CalDAV server.
ZiB 4 månader sedan
förälder
incheckning
668d07f087
3 ändrade filer med 86 tillägg och 4 borttagningar
  1. 1 0
      pyproject.toml
  2. 84 4
      tests/conftest.py
  3. 1 0
      tests/test_action_chains.py

+ 1 - 0
pyproject.toml

@@ -36,6 +36,7 @@ dev = [
   "pytest",
   "pytest-asyncio",
   "pytest-xdist",
+  "radicale",
 ]
 
 [tool.hatch.build.targets.wheel]

+ 84 - 4
tests/conftest.py

@@ -1,9 +1,89 @@
+import dataclasses
+import socket
+import subprocess
+import sys
+import time
+
+import httpx
 import pytest
-from room_cal._utils import get_config
+
+
+def _free_port() -> int:
+  with socket.socket() as s:
+    s.bind(('127.0.0.1', 0))
+    return s.getsockname()[1]
+
+
+@pytest.fixture(scope='session')
+def radicale_server(tmp_path_factory):
+  """Start a local Radicale CalDAV server for the test session.
+
+  auth.type = none accepts any credentials; the username in the HTTP auth
+  still determines the path prefix (http://host/{username}/), so we always
+  use username='test'. The 'House' calendar is created on first use; any
+  test that writes to it cleans up in its own finally block.
+  """
+  port = _free_port()
+  data_dir = tmp_path_factory.mktemp('radicale')
+  config_ini = data_dir / 'config.ini'
+  config_ini.write_text('\n'.join([
+    '[server]',
+    f'hosts = 127.0.0.1:{port}',
+    '[auth]',
+    'type = none',
+    '[storage]',
+    f'filesystem_folder = {data_dir / "collections"}',
+  ]))
+
+  proc = subprocess.Popen(
+    [sys.executable, '-m', 'radicale', '--config', str(config_ini)],
+    stdout=subprocess.DEVNULL,
+    stderr=subprocess.DEVNULL,
+  )
+
+  url = f'http://127.0.0.1:{port}/'
+  for _ in range(50):
+    try:
+      httpx.get(url, timeout=1.0)
+      break
+    except Exception:
+      time.sleep(0.1)
+  else:
+    proc.kill()
+    pytest.fail('Radicale did not start within 5 seconds')
+
+  import caldav
+  client = caldav.DAVClient(url=url, username='test', password='test')
+  principal = client.principal()
+  principal.make_calendar(name='House', cal_id='house')
+
+  yield {'url': url, 'username': 'test', 'password': 'test', 'calendar_name': 'House'}
+
+  proc.terminate()
+  try:
+    proc.wait(timeout=5)
+  except subprocess.TimeoutExpired:
+    proc.kill()
 
 
 @pytest.fixture
-def require_user_cal():
+def require_user_cal(radicale_server, monkeypatch):
+  """Redirect user_cal to the local Radicale server instead of skipping.
+
+  Patches get_config in the two modules that call it on the user_cal path:
+    - room_cal.user_cal          (UserCal.__init__)
+    - room_cal.actions.fetch_user_cal  (calendar_name lookup)
+  """
+  from room_cal._utils import get_config
   cfg = get_config()
-  if not getattr(cfg, 'user_cal', None) or not cfg.user_cal.url:
-    pytest.skip('user_cal CalDAV not configured')
+  test_user_cal_cfg = dataclasses.replace(
+    cfg.user_cal,
+    url=radicale_server['url'],
+    username=radicale_server['username'],
+    password=radicale_server['password'],
+    calendar_name=radicale_server['calendar_name'],
+  )
+  test_cfg = dataclasses.replace(cfg, user_cal=test_user_cal_cfg)
+  mock = lambda: test_cfg
+  monkeypatch.setattr('room_cal.user_cal.get_config', mock)
+  monkeypatch.setattr('room_cal.actions.fetch_user_cal.get_config', mock)

+ 1 - 0
tests/test_action_chains.py

@@ -284,6 +284,7 @@ async def test_apply_discounts_pushes_discounted_prices():
 
 
 @pytest.mark.asyncio
+@pytest.mark.usefixtures('require_user_cal')
 async def test_diff_returns_complete_structure():
   """diff() on a past window returns all expected keys with valid list/dict shapes."""
   from room_cal.service_api import ServiceAPI