Add unit test suite with pytest configuration
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
259
tests/unit/test_obsidian_service.py
Normal file
259
tests/unit/test_obsidian_service.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""Tests for ObsidianService markdown parsing and file operations."""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
# Set vault path before importing so __init__ validation passes
|
||||
_test_vault_dir = None
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def vault_dir(tmp_path):
|
||||
"""Create a temporary vault directory with a sample .md file."""
|
||||
global _test_vault_dir
|
||||
_test_vault_dir = tmp_path
|
||||
|
||||
# Create a sample markdown file so vault validation passes
|
||||
sample = tmp_path / "sample.md"
|
||||
sample.write_text("# Sample\nHello world")
|
||||
|
||||
with patch.dict(os.environ, {"OBSIDIAN_VAULT_PATH": str(tmp_path)}):
|
||||
yield tmp_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service(vault_dir):
|
||||
from utils.obsidian_service import ObsidianService
|
||||
|
||||
return ObsidianService()
|
||||
|
||||
|
||||
class TestParseMarkdown:
|
||||
def test_extracts_frontmatter(self, service):
|
||||
content = "---\ntitle: Test Note\ntags: [cat, vet]\n---\n\nBody content"
|
||||
result = service.parse_markdown(content)
|
||||
assert result["metadata"]["title"] == "Test Note"
|
||||
assert result["metadata"]["tags"] == ["cat", "vet"]
|
||||
|
||||
def test_no_frontmatter(self, service):
|
||||
content = "Just body content with no frontmatter"
|
||||
result = service.parse_markdown(content)
|
||||
assert result["metadata"] == {}
|
||||
assert "Just body content" in result["content"]
|
||||
|
||||
def test_invalid_yaml_frontmatter(self, service):
|
||||
content = "---\n: invalid: yaml: [[\n---\n\nBody"
|
||||
result = service.parse_markdown(content)
|
||||
assert result["metadata"] == {}
|
||||
|
||||
def test_extracts_tags(self, service):
|
||||
content = "Some text with #tag1 and #tag2 here"
|
||||
result = service.parse_markdown(content)
|
||||
assert "tag1" in result["tags"]
|
||||
assert "tag2" in result["tags"]
|
||||
|
||||
def test_extracts_wikilinks(self, service):
|
||||
content = "Link to [[Other Note]] and [[Another Page]]"
|
||||
result = service.parse_markdown(content)
|
||||
assert "Other Note" in result["wikilinks"]
|
||||
assert "Another Page" in result["wikilinks"]
|
||||
|
||||
def test_extracts_embeds(self, service):
|
||||
content = "An embed [[!my_embed]] here"
|
||||
result = service.parse_markdown(content)
|
||||
assert "my_embed" in result["embeds"]
|
||||
|
||||
def test_cleans_wikilinks_from_content(self, service):
|
||||
content = "Text with [[link]] included"
|
||||
result = service.parse_markdown(content)
|
||||
assert "[[" not in result["content"]
|
||||
assert "]]" not in result["content"]
|
||||
|
||||
def test_filepath_passed_through(self, service):
|
||||
result = service.parse_markdown("text", filepath=Path("/vault/note.md"))
|
||||
assert result["filepath"] == "/vault/note.md"
|
||||
|
||||
def test_filepath_none_by_default(self, service):
|
||||
result = service.parse_markdown("text")
|
||||
assert result["filepath"] is None
|
||||
|
||||
def test_empty_content(self, service):
|
||||
result = service.parse_markdown("")
|
||||
assert result["metadata"] == {}
|
||||
assert result["tags"] == []
|
||||
assert result["wikilinks"] == []
|
||||
assert result["embeds"] == []
|
||||
|
||||
|
||||
class TestGetDailyNotePath:
|
||||
def test_formats_path_correctly(self, service):
|
||||
date = datetime(2026, 3, 15)
|
||||
path = service.get_daily_note_path(date)
|
||||
assert path == "journal/2026/2026-03-15.md"
|
||||
|
||||
def test_defaults_to_today(self, service):
|
||||
path = service.get_daily_note_path()
|
||||
today = datetime.now()
|
||||
assert today.strftime("%Y-%m-%d") in path
|
||||
assert path.startswith(f"journal/{today.strftime('%Y')}/")
|
||||
|
||||
|
||||
class TestWalkVault:
|
||||
def test_finds_markdown_files(self, service, vault_dir):
|
||||
(vault_dir / "note1.md").write_text("# Note 1")
|
||||
(vault_dir / "subdir").mkdir()
|
||||
(vault_dir / "subdir" / "note2.md").write_text("# Note 2")
|
||||
|
||||
files = service.walk_vault()
|
||||
filenames = [f.name for f in files]
|
||||
assert "sample.md" in filenames
|
||||
assert "note1.md" in filenames
|
||||
assert "note2.md" in filenames
|
||||
|
||||
def test_excludes_obsidian_dir(self, service, vault_dir):
|
||||
obsidian_dir = vault_dir / ".obsidian"
|
||||
obsidian_dir.mkdir()
|
||||
(obsidian_dir / "config.md").write_text("config")
|
||||
|
||||
files = service.walk_vault()
|
||||
filenames = [f.name for f in files]
|
||||
assert "config.md" not in filenames
|
||||
|
||||
def test_ignores_non_md_files(self, service, vault_dir):
|
||||
(vault_dir / "image.png").write_bytes(b"\x89PNG")
|
||||
|
||||
files = service.walk_vault()
|
||||
filenames = [f.name for f in files]
|
||||
assert "image.png" not in filenames
|
||||
|
||||
|
||||
class TestCreateNote:
|
||||
def test_creates_file(self, service, vault_dir):
|
||||
path = service.create_note("My Test Note", "Body content")
|
||||
full_path = vault_dir / path
|
||||
assert full_path.exists()
|
||||
|
||||
def test_sanitizes_title(self, service, vault_dir):
|
||||
path = service.create_note("Hello World! @#$", "Body")
|
||||
assert "hello-world" in path
|
||||
assert "@" not in path
|
||||
assert "#" not in path
|
||||
|
||||
def test_includes_frontmatter(self, service, vault_dir):
|
||||
path = service.create_note("Test", "Body", tags=["cat", "vet"])
|
||||
full_path = vault_dir / path
|
||||
content = full_path.read_text()
|
||||
assert "---" in content
|
||||
assert "created_by: simbarag" in content
|
||||
assert "cat" in content
|
||||
assert "vet" in content
|
||||
|
||||
def test_custom_folder(self, service, vault_dir):
|
||||
path = service.create_note("Test", "Body", folder="custom/subfolder")
|
||||
assert path.startswith("custom/subfolder/")
|
||||
assert (vault_dir / path).exists()
|
||||
|
||||
|
||||
class TestDailyNoteTasks:
|
||||
def test_get_tasks_from_daily_note(self, service, vault_dir):
|
||||
# Create a daily note with tasks
|
||||
date = datetime(2026, 1, 15)
|
||||
rel_path = service.get_daily_note_path(date)
|
||||
note_path = vault_dir / rel_path
|
||||
note_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
note_path.write_text(
|
||||
"---\nmodified: 2026-01-15\n---\n"
|
||||
"### tasks\n\n"
|
||||
"- [ ] Feed the cat\n"
|
||||
"- [x] Clean litter box\n"
|
||||
"- [ ] Buy cat food\n\n"
|
||||
"### log\n"
|
||||
)
|
||||
|
||||
result = service.get_daily_tasks(date)
|
||||
assert result["found"] is True
|
||||
assert len(result["tasks"]) == 3
|
||||
assert result["tasks"][0] == {"text": "Feed the cat", "done": False}
|
||||
assert result["tasks"][1] == {"text": "Clean litter box", "done": True}
|
||||
assert result["tasks"][2] == {"text": "Buy cat food", "done": False}
|
||||
|
||||
def test_get_tasks_no_note(self, service):
|
||||
date = datetime(2099, 12, 31)
|
||||
result = service.get_daily_tasks(date)
|
||||
assert result["found"] is False
|
||||
assert result["tasks"] == []
|
||||
|
||||
def test_add_task_creates_note(self, service, vault_dir):
|
||||
date = datetime(2026, 6, 1)
|
||||
result = service.add_task_to_daily_note("Walk the cat", date)
|
||||
assert result["success"] is True
|
||||
assert result["created_note"] is True
|
||||
|
||||
# Verify file was created with the task
|
||||
note_path = vault_dir / result["path"]
|
||||
content = note_path.read_text()
|
||||
assert "- [ ] Walk the cat" in content
|
||||
|
||||
def test_add_task_to_existing_note(self, service, vault_dir):
|
||||
date = datetime(2026, 6, 2)
|
||||
rel_path = service.get_daily_note_path(date)
|
||||
note_path = vault_dir / rel_path
|
||||
note_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
note_path.write_text(
|
||||
"---\nmodified: 2026-06-02\n---\n"
|
||||
"### tasks\n\n"
|
||||
"- [ ] Existing task\n\n"
|
||||
"### log\n"
|
||||
)
|
||||
|
||||
result = service.add_task_to_daily_note("New task", date)
|
||||
assert result["success"] is True
|
||||
assert result["created_note"] is False
|
||||
|
||||
content = note_path.read_text()
|
||||
assert "- [ ] Existing task" in content
|
||||
assert "- [ ] New task" in content
|
||||
|
||||
def test_complete_task_exact_match(self, service, vault_dir):
|
||||
date = datetime(2026, 6, 3)
|
||||
rel_path = service.get_daily_note_path(date)
|
||||
note_path = vault_dir / rel_path
|
||||
note_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
note_path.write_text("### tasks\n\n" "- [ ] Feed the cat\n" "- [ ] Buy food\n")
|
||||
|
||||
result = service.complete_task_in_daily_note("Feed the cat", date)
|
||||
assert result["success"] is True
|
||||
|
||||
content = note_path.read_text()
|
||||
assert "- [x] Feed the cat" in content
|
||||
assert "- [ ] Buy food" in content # Other task unchanged
|
||||
|
||||
def test_complete_task_partial_match(self, service, vault_dir):
|
||||
date = datetime(2026, 6, 4)
|
||||
rel_path = service.get_daily_note_path(date)
|
||||
note_path = vault_dir / rel_path
|
||||
note_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
note_path.write_text("### tasks\n\n- [ ] Feed the cat at 5pm\n")
|
||||
|
||||
result = service.complete_task_in_daily_note("Feed the cat", date)
|
||||
assert result["success"] is True
|
||||
|
||||
def test_complete_task_not_found(self, service, vault_dir):
|
||||
date = datetime(2026, 6, 5)
|
||||
rel_path = service.get_daily_note_path(date)
|
||||
note_path = vault_dir / rel_path
|
||||
note_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
note_path.write_text("### tasks\n\n- [ ] Feed the cat\n")
|
||||
|
||||
result = service.complete_task_in_daily_note("Walk the dog", date)
|
||||
assert result["success"] is False
|
||||
assert "not found" in result["error"]
|
||||
|
||||
def test_complete_task_no_note(self, service):
|
||||
date = datetime(2099, 12, 31)
|
||||
result = service.complete_task_in_daily_note("Something", date)
|
||||
assert result["success"] is False
|
||||
Reference in New Issue
Block a user