conftest.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. import dataclasses
  2. import os
  3. import socket
  4. import subprocess
  5. import sys
  6. import time
  7. import httpx
  8. import pytest
  9. def pytest_collection_modifyitems(config, items):
  10. """Skip tests marked 'live' unless ROOM_CAL_LIVE_TESTS=1.
  11. Live tests hit real service APIs and need credentials that dev systems
  12. only sometimes have (especially for 2FA-gated backends).
  13. """
  14. if os.environ.get('ROOM_CAL_LIVE_TESTS'):
  15. return
  16. skip_live = pytest.mark.skip(reason='live service-api test; set ROOM_CAL_LIVE_TESTS=1 to run')
  17. for item in items:
  18. if 'live' in item.keywords:
  19. item.add_marker(skip_live)
  20. @pytest.fixture(autouse=True)
  21. def _reset_service_api_singleton():
  22. """Drop the ServiceAPI singleton after each test so stubs and creds can't leak."""
  23. yield
  24. import room_cal.service_api.client as _client
  25. _client._instance = None
  26. def _free_port() -> int:
  27. with socket.socket() as s:
  28. s.bind(('127.0.0.1', 0))
  29. return s.getsockname()[1]
  30. @pytest.fixture(scope='session')
  31. def radicale_server(tmp_path_factory):
  32. """Start a local Radicale CalDAV server for the test session.
  33. auth.type = none accepts any credentials; the username in the HTTP auth
  34. still determines the path prefix (http://host/{username}/), so we always
  35. use username='test'. The 'House' calendar is created on first use; any
  36. test that writes to it cleans up in its own finally block.
  37. """
  38. port = _free_port()
  39. data_dir = tmp_path_factory.mktemp('radicale')
  40. config_ini = data_dir / 'config.ini'
  41. config_ini.write_text('\n'.join([
  42. '[server]',
  43. f'hosts = 127.0.0.1:{port}',
  44. '[auth]',
  45. 'type = none',
  46. '[storage]',
  47. f'filesystem_folder = {data_dir / "collections"}',
  48. ]))
  49. proc = subprocess.Popen(
  50. [sys.executable, '-m', 'radicale', '--config', str(config_ini)],
  51. stdout=subprocess.DEVNULL,
  52. stderr=subprocess.DEVNULL,
  53. )
  54. url = f'http://127.0.0.1:{port}/'
  55. for _ in range(50):
  56. try:
  57. httpx.get(url, timeout=1.0)
  58. break
  59. except Exception:
  60. time.sleep(0.1)
  61. else:
  62. proc.kill()
  63. pytest.fail('Radicale did not start within 5 seconds')
  64. import caldav
  65. client = caldav.DAVClient(url=url, username='test', password='test')
  66. principal = client.principal()
  67. principal.make_calendar(name='House', cal_id='house')
  68. yield {'url': url, 'username': 'test', 'password': 'test', 'calendar_name': 'House'}
  69. proc.terminate()
  70. try:
  71. proc.wait(timeout=5)
  72. except subprocess.TimeoutExpired:
  73. proc.kill()
  74. @pytest.fixture
  75. def require_user_cal(radicale_server, monkeypatch):
  76. """Redirect user_cal to the local Radicale server instead of skipping.
  77. Patches get_config in the two modules that call it on the user_cal path:
  78. - room_cal.user_cal (UserCal.__init__)
  79. - room_cal.actions.fetch_user_cal (calendar_name lookup)
  80. """
  81. from room_cal._utils import get_config
  82. cfg = get_config()
  83. test_user_cal_cfg = dataclasses.replace(
  84. cfg.user_cal,
  85. url=radicale_server['url'],
  86. username=radicale_server['username'],
  87. password=radicale_server['password'],
  88. calendar_name=radicale_server['calendar_name'],
  89. )
  90. test_cfg = dataclasses.replace(cfg, user_cal=test_user_cal_cfg)
  91. mock = lambda: test_cfg
  92. monkeypatch.setattr('room_cal.user_cal.get_config', mock)
  93. monkeypatch.setattr('room_cal.actions.fetch_user_cal.get_config', mock)
  94. # reset the memoized user_cal Calendar so it's rebuilt from the patched
  95. # config (and restored after the test)
  96. monkeypatch.setattr('room_cal.user_cal._user_calendar', None)