From a2c5cf281b864b53123ed8bdcd081fd2e6fad153 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Sat, 29 Nov 2025 21:06:09 +0100 Subject: [PATCH 1/3] fix: Remove receiving output from io.err method The method never returns anything so we should not receive output. --- topen.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/topen.py b/topen.py index b5374e1..01b8f8f 100755 --- a/topen.py +++ b/topen.py @@ -51,14 +51,14 @@ def main(cfg: "TConf | None" = None, io: "_IO | None" = None) -> int: io = _IO(quiet=cfg.notes_quiet) if not cfg.task_id: - _ = io.err("Please provide task ID as argument.\n") + io.err("Please provide task ID as argument.\n") return 1 try: task = get_task(id=cfg.task_id, data_location=cfg.task_data) uuid = cast(str, task["uuid"]) except Task.DoesNotExist: - _ = io.err(f"Could not find task for ID: {cfg.task_id}.\n") + io.err(f"Could not find task for ID: {cfg.task_id}.\n") return 1 fpath = get_notes_file(uuid, notes_dir=cfg.notes_dir, notes_ext=cfg.notes_ext) From 0820b686e502f75c40766a8ab5cbb9c550e09247 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 8 Dec 2025 17:22:26 +0100 Subject: [PATCH 2/3] fix: Add edge case tests and improve write permissions error --- test/test_problems_edge_cases.py | 64 ++++++++++++++++++++++++++++++++ topen.py | 7 +++- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 test/test_problems_edge_cases.py diff --git a/test/test_problems_edge_cases.py b/test/test_problems_edge_cases.py new file mode 100644 index 0000000..1d787af --- /dev/null +++ b/test/test_problems_edge_cases.py @@ -0,0 +1,64 @@ +import configparser +import os +import tempfile +from pathlib import Path +from unittest.mock import patch + +import pytest + +from topen import _ensure_parent_dir, get_notes_file, get_task, parse_env, parse_rc + + +class TestFSEdgeCases: + """Test edge cases for TaskWarrior integration and file system operations.""" + + def test_nonexistent_task_id(self, tmp_path): + """Test raised error for non-existent task IDs.""" + with patch("tasklib.TaskWarrior") as mock_tw: + mock_tw.return_value.tasks.get.side_effect = [ + Exception("Task not found"), + ] + + with pytest.raises( + Exception, + match="Task matching query does not exist. Lookup parameters were {'uuid': '999999'}", + ): + get_task("999999", tmp_path) + + def test_read_only_notes_directory(self, tmp_path): + """Test raised error when notes directory is read-only.""" + notes_dir = tmp_path / "read_only_notes" + notes_dir.mkdir() + + # Make directory read-only + os.chmod(notes_dir, 0o444) + + fpath = notes_dir / "subdir_cant_be_written" / "uuid.md" + with pytest.raises(PermissionError): + _ensure_parent_dir(fpath) + + def test_symlink_notes_directory(self, tmp_path): + """Test behavior with symlinked notes directory, + reading the linked dir instead of the real dir. """ + real_dir = tmp_path / "real_notes" + real_dir.mkdir() + link_dir = tmp_path / "linked_notes" + link_dir.symlink_to(real_dir) + + fpath = get_notes_file("test-uuid", link_dir, "md") + assert fpath.parent == link_dir + + def test_empty_taskrc_file(self, tmp_path): + """Test functional handling of empty taskrc file.""" + fake_rc: Path = tmp_path / "empty.taskrc" + fake_rc.touch() + assert parse_rc(fake_rc) == {} + + def test_taskrc_with_invalid_syntax(self, tmp_path): + """Test functional handling of taskrc with invalid syntax.""" + invalid_rc: Path = tmp_path / "invalid.taskrc" + invalid_rc.write_text( + "invalid line == [MMMM] with too many = = equals sign\ndata.location = valid_value\n" + ) + + assert parse_rc(invalid_rc) diff --git a/topen.py b/topen.py index 01b8f8f..ae56f93 100755 --- a/topen.py +++ b/topen.py @@ -63,7 +63,12 @@ def main(cfg: "TConf | None" = None, io: "_IO | None" = None) -> int: fpath = get_notes_file(uuid, notes_dir=cfg.notes_dir, notes_ext=cfg.notes_ext) - _ensure_parent_dir(fpath) + try: + _ensure_parent_dir(fpath) + except PermissionError: + io.err(f"Could not write required directories for path: {fpath}.\n") + return 1 + io.out(f"Editing note: {fpath}") open_editor(fpath, editor=cfg.notes_editor, io=io) From f00d230fd38d0f0c638d011813db2f6ee1e5ecc0 Mon Sep 17 00:00:00 2001 From: Marty Oehme Date: Mon, 8 Dec 2025 17:38:23 +0100 Subject: [PATCH 3/3] test: Add test for parsing circular env vars --- test/test_configuration.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/test_configuration.py b/test/test_configuration.py index 7afe8c8..a4573c6 100644 --- a/test/test_configuration.py +++ b/test/test_configuration.py @@ -7,6 +7,7 @@ import pytest from topen import TConf, build_config + class TestTConf: def test_paths_are_expanded(self): cfg = TConf.from_dict( @@ -62,6 +63,17 @@ class TestBuildConfigPrecedence: cfg = build_config() assert cfg.notes_ext == "from-env" + def test_circular_env_vars(self, isolate_env, monkeypatch, fake_id): + """Test environment variables with circular references.""" + for k, v in { + "TOPEN_NOTES_DIR": "$TOPEN_NOTES_DIR/subdir", + "EDITOR": "${EDITOR}_backup", + }.items(): + monkeypatch.setenv(k, v) + cfg = build_config() + assert cfg.notes_dir == Path("$TOPEN_NOTES_DIR/subdir/subdir") + assert cfg.notes_editor == "nano" + def test_cli_overrides_env(self, fake_rc, monkeypatch, isolate_env): fake_rc.write_text("notes.ext=from-rc\n") monkeypatch.setenv("TOPEN_NOTES_EXT", "from-env")