""" Tests for the Joplin integration module. All tests mock the joppy ClientApi so no live Joplin instance is required. """ from datetime import date from unittest.mock import MagicMock, patch import pytest from timesheets.joplin import _iso_week_label, fetch_week_note # --------------------------------------------------------------------------- # _iso_week_label # --------------------------------------------------------------------------- class TestIsoWeekLabel: @pytest.mark.parametrize( "d, expected", [ (date(2026, 5, 22), "2026 - W21"), (date(2026, 5, 18), "2026 - W21"), # Monday of same week (date(2026, 1, 1), "2026 - W01"), (date(2026, 12, 28), "2026 - W53"), ], ) def test_label(self, d, expected): assert _iso_week_label(d) == expected # --------------------------------------------------------------------------- # fetch_week_note — mocked API # --------------------------------------------------------------------------- def _make_notebook(id_, title, parent_id=""): nb = MagicMock() nb.id = id_ nb.title = title nb.parent_id = parent_id return nb def _make_note(id_, title, body): note = MagicMock() note.id = id_ note.title = title note.body = body return note def _build_api(note_body: str, week_label: str = "2026 - W21"): """Return a mocked ClientApi that serves a minimal notebook tree.""" api = MagicMock() api.get_all_notebooks.return_value = [ _make_notebook("work_id", "Work", parent_id=""), _make_notebook("ts_id", "Timesheets", parent_id="work_id"), _make_notebook("yr_id", "2026", parent_id="ts_id"), ] api.get_all_notes.return_value = [ _make_note("note_id", week_label, note_body), ] return api class TestFetchWeekNote: def _patch(self, api): return patch("timesheets.joplin.ClientApi", return_value=api) def test_returns_note_body(self): api = _build_api("# content") with self._patch(api): result = fetch_week_note("dummy_token", date(2026, 5, 22)) assert result == "# content" def test_constructs_client_with_token(self): api = _build_api("body") with patch("timesheets.joplin.ClientApi", return_value=api) as mock_cls: fetch_week_note("my_secret_token", date(2026, 5, 22)) mock_cls.assert_called_once_with(token="my_secret_token") def test_missing_work_notebook_raises(self): api = MagicMock() api.get_all_notebooks.return_value = [] with self._patch(api): with pytest.raises(RuntimeError, match="'Work' not found"): fetch_week_note("token", date(2026, 5, 22)) def test_missing_timesheets_notebook_raises(self): api = MagicMock() api.get_all_notebooks.return_value = [ _make_notebook("work_id", "Work", parent_id=""), ] with self._patch(api): with pytest.raises(RuntimeError, match="Timesheets"): fetch_week_note("token", date(2026, 5, 22)) def test_missing_year_notebook_raises(self): api = MagicMock() api.get_all_notebooks.return_value = [ _make_notebook("work_id", "Work", parent_id=""), _make_notebook("ts_id", "Timesheets", parent_id="work_id"), ] with self._patch(api): with pytest.raises(RuntimeError, match="2026"): fetch_week_note("token", date(2026, 5, 22)) def test_missing_note_raises(self): api = _build_api("body", week_label="2026 - W21") # Return a note with the wrong title api.get_all_notes.return_value = [_make_note("x", "wrong title", "body")] with self._patch(api): with pytest.raises(RuntimeError, match="note '2026 - W21' not found"): fetch_week_note("token", date(2026, 5, 22))