- Add joplin.py with fetch_week_note() that walks Work > Timesheets > YYYY and returns the body of the matching YYYY-WNN note via joppy ClientApi - Add filter_rows_by_date() to parser.py to extract only rows belonging to a specific day based on '# ... YYYY-MM-DD' headings in the document - Update cli.py: input and --joplin are now a mutually exclusive required group; add --token flag with JOPLIN_TOKEN env var fallback; --date is parsed into a real date object used for both output and day filtering - Add joppy as a runtime dependency (lazy-imported in cli.py) - Add tests for filter_rows_by_date and full mocked coverage of joplin.py - Update AGENTS.md with Joplin usage, notebook structure, and test rules The actual Joplin structure has notes directly inside the year notebook (Work > Timesheets > YYYY), not in per-week sub-notebooks as initially assumed. fetch_week_note() reflects this flat structure.
116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
"""
|
|
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))
|