Files
ad_trident/tests/test_alarmclock.py
T
2026-11-13 13:10:37 +01:00

145 lines
6.3 KiB
Python

from contextlib import nullcontext
import datetime
import pickle
import tempfile
from pathlib import Path
import sys
import unittest
from unittest.mock import Mock, patch
sys.path[:0] = [str(Path(__file__).resolve().parents[1] / 'apps'),
str(Path(__file__).resolve().parents[1] / 'apps/ad_toolbox')]
from alarmclock import AlarmClock
class Harness(AlarmClock):
def __init__(self):
self.calendar = ['previous calendar']
self.calendar_retry_handle = None
self.calendar_retry_attempt = 0
self.dataset = {}
self.load_cal_from_ics = Mock(return_value=False)
self.run_in = Mock(side_effect=lambda callback, delay: f'timer-{delay}')
self.timer_running = Mock(return_value=True)
self.cancel_timer = Mock()
self.log_info = Mock()
self.log_warning = Mock()
self.log_error = Mock()
self.log_group = Mock(side_effect=lambda name: nullcontext())
self.update_workday_status = Mock()
self.update_wakeup_time = Mock()
class CalendarRefreshTests(unittest.TestCase):
def test_last_success_survives_dataset_save_and_restore(self):
app = Harness()
app.calendar = []
app.load_cal_from_ics.return_value = True
app.refresh_cal()
last_success = app.calendar_last_success
self.assertIsNotNone(last_success)
with tempfile.TemporaryDirectory() as folder:
dataset_path = Path(folder) / 'alarm_clock.dataset'
app.get_dataset_name = Mock(return_value=str(dataset_path))
app.terminate()
restored = Harness()
with dataset_path.open('rb') as stream:
restored.dataset = pickle.load(stream)
self.assertEqual(restored.calendar_last_success, last_success)
restored._log_calendar_failure('request timed out')
restored.log_info.assert_called_once()
restored.log_warning.assert_not_called()
restored.log_error.assert_not_called()
def test_dataset_without_timestamp_has_no_known_success(self):
app = Harness()
app.dataset = {'other_data': 42}
self.assertIsNone(app.calendar_last_success)
app._log_calendar_failure('request timed out')
app.log_error.assert_called_once()
self.assertEqual(app.dataset, {'other_data': 42})
def test_failure_preserves_calendar_and_schedules_two_retries(self):
app = Harness()
self.assertFalse(app.refresh_cal())
self.assertEqual(app.calendar, ['previous calendar'])
app.run_in.assert_called_once_with(app.on_calendar_retry, 120)
app.on_calendar_retry({})
self.assertEqual(app.run_in.call_args_list[-1].args, (app.on_calendar_retry, 300))
app.on_calendar_retry({})
self.assertEqual(app.run_in.call_count, 2)
def test_failure_level_depends_on_last_success(self):
now = datetime.datetime(2026, 11, 4, 12, tzinfo=datetime.timezone.utc)
for hours, level in ((0, 'info'), (23.999, 'info'), (24, 'warning'),
(47.999, 'warning'), (48, 'error'), (72, 'error'),
(None, 'error')):
with self.subTest(hours=hours):
app = Harness()
last_success = None if hours is None else now - datetime.timedelta(hours=hours)
app.calendar_last_success = last_success
with patch('alarmclock.datetime.datetime') as clock:
clock.now.return_value = now
app._log_calendar_failure('request timed out')
getattr(app, f'log_{level}').assert_called_once()
for other in {'info', 'warning', 'error'} - {level}:
getattr(app, f'log_{other}').assert_not_called()
self.assertEqual(app.calendar_last_success, last_success)
message = getattr(app, f'log_{level}').call_args.args[0]
self.assertIn('request timed out', message)
if last_success:
self.assertIn(last_success.isoformat(), message)
def test_success_updates_timestamp_even_when_calendar_is_unchanged(self):
app = Harness()
app.calendar = []
app.calendar_last_success = datetime.datetime(2026, 11, 1, tzinfo=datetime.timezone.utc)
app.calendar_retry_attempt = 2
app.calendar_retry_handle = 'pending-timer'
app.load_cal_from_ics.return_value = True
now = datetime.datetime(2026, 11, 4, tzinfo=datetime.timezone.utc)
with patch('alarmclock.datetime.datetime') as clock:
clock.now.return_value = now
self.assertFalse(app.refresh_cal())
app.cancel_timer.assert_called_once_with('pending-timer')
self.assertEqual(app.calendar_last_success, now)
self.assertEqual(app.calendar_retry_attempt, 0)
@patch('alarmclock.get')
def test_timeout_uses_freshness_level_and_preserves_calendar(self, get):
import requests
app = Harness()
app.calendar_last_success = datetime.datetime.now(datetime.timezone.utc)
last_success = app.calendar_last_success
app.load_cal_from_ics = lambda calendar: AlarmClock.load_cal_from_ics(app, calendar)
get.side_effect = requests.exceptions.ReadTimeout()
self.assertFalse(app.refresh_cal())
app.log_warning.assert_not_called()
app.log_error.assert_not_called()
self.assertTrue(any('ReadTimeout' in call.args[0] for call in app.log_info.call_args_list))
self.assertEqual(app.calendar_last_success, last_success)
self.assertEqual(app.calendar, ['previous calendar'])
app.run_in.assert_called_once_with(app.on_calendar_retry, 120)
@patch('alarmclock.icalendar.Calendar.from_ical')
@patch('alarmclock.get')
def test_http_request_uses_short_timeouts_and_validates_status(self, get, from_ical):
response = Mock(content=b'BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n')
get.return_value = response
from_ical.return_value.walk.return_value = []
app = Harness()
self.assertTrue(AlarmClock.load_cal_from_ics(app, []))
self.assertEqual(get.call_args.kwargs['timeout'], (3, 5))
response.raise_for_status.assert_called_once_with()
from_ical.assert_called_once_with(response.content)
if __name__ == '__main__':
unittest.main()