Add comprehensive test suite for MVP-1 Personal Tracker CLI
Implements 72 tests covering: - Model tests (Project, Session, Note, Change) - ProjectService tests (create, get, list, ensure structure) - SessionService tests (active session management) - FileStorage tests (read/write operations) - Complete flow tests (init -> start -> note -> stop -> show) - Note consolidation tests Uses pytest with tmp_path fixtures for isolated testing.
This commit is contained in:
209
tests/test_project_service.py
Normal file
209
tests/test_project_service.py
Normal file
@@ -0,0 +1,209 @@
|
||||
"""Tests for ProjectService."""
|
||||
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
from tracker.models import Project
|
||||
from tracker.services import (
|
||||
create_project,
|
||||
get_project,
|
||||
update_project,
|
||||
list_projects,
|
||||
get_projects_root,
|
||||
ensure_project_structure,
|
||||
)
|
||||
|
||||
|
||||
class TestCreateProject:
|
||||
"""Tests for create_project function."""
|
||||
|
||||
def test_create_project_returns_project_instance(self):
|
||||
"""Test that create_project returns a Project instance."""
|
||||
project = create_project(
|
||||
name="Test Project",
|
||||
slug="test-project",
|
||||
description="A test project",
|
||||
type="code",
|
||||
)
|
||||
|
||||
assert isinstance(project, Project)
|
||||
assert project.name == "Test Project"
|
||||
assert project.slug == "test-project"
|
||||
assert project.description == "A test project"
|
||||
assert project.type == "code"
|
||||
|
||||
def test_create_project_generates_id(self):
|
||||
"""Test that create_project generates a UUID."""
|
||||
project = create_project(name="Test", slug="test")
|
||||
assert project.id is not None
|
||||
assert len(project.id) > 0
|
||||
|
||||
def test_create_project_sets_default_status(self):
|
||||
"""Test that create_project sets default status to inbox."""
|
||||
project = create_project(name="Test", slug="test")
|
||||
assert project.status == "inbox"
|
||||
|
||||
def test_create_project_sets_timestamps(self):
|
||||
"""Test that create_project sets created_at and updated_at."""
|
||||
before = datetime.now()
|
||||
project = create_project(name="Test", slug="test")
|
||||
after = datetime.now()
|
||||
|
||||
assert before <= project.created_at <= after
|
||||
assert before <= project.updated_at <= after
|
||||
# created_at and updated_at should be very close (within 1 second)
|
||||
time_diff = abs((project.updated_at - project.created_at).total_seconds())
|
||||
assert time_diff < 1
|
||||
|
||||
def test_create_project_with_tags(self):
|
||||
"""Test creating a project with tags."""
|
||||
tags = ["python", "testing", "cli"]
|
||||
project = create_project(name="Test", slug="test", tags=tags)
|
||||
assert project.tags == tags
|
||||
|
||||
def test_create_project_with_repo_path(self):
|
||||
"""Test creating a project with a repo path."""
|
||||
repo_path = Path("/path/to/repo")
|
||||
project = create_project(name="Test", slug="test", repo_path=repo_path)
|
||||
assert project.repo_path == str(repo_path)
|
||||
|
||||
def test_create_project_without_repo_path(self):
|
||||
"""Test creating a project without repo path."""
|
||||
project = create_project(name="Test", slug="test")
|
||||
assert project.repo_path is None
|
||||
|
||||
|
||||
class TestGetProject:
|
||||
"""Tests for get_project function."""
|
||||
|
||||
def test_get_project_returns_none_for_nonexistent(self, tmp_path):
|
||||
"""Test that get_project returns None for nonexistent project."""
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
result = get_project("nonexistent")
|
||||
assert result is None
|
||||
|
||||
def test_get_project_returns_project_when_exists(self, tmp_path, sample_project_data):
|
||||
"""Test that get_project returns Project when it exists."""
|
||||
slug = sample_project_data["slug"]
|
||||
project_dir = tmp_path / slug / "meta"
|
||||
project_dir.mkdir(parents=True)
|
||||
|
||||
meta_path = project_dir / "project.yaml"
|
||||
with open(meta_path, "w") as f:
|
||||
yaml.safe_dump(sample_project_data, f)
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
project = get_project(slug)
|
||||
assert project is not None
|
||||
assert project.slug == slug
|
||||
assert project.name == sample_project_data["name"]
|
||||
|
||||
def test_get_project_handles_invalid_yaml(self, tmp_path):
|
||||
"""Test that get_project handles invalid YAML gracefully."""
|
||||
slug = "test-project"
|
||||
project_dir = tmp_path / slug / "meta"
|
||||
project_dir.mkdir(parents=True)
|
||||
|
||||
meta_path = project_dir / "project.yaml"
|
||||
with open(meta_path, "w") as f:
|
||||
f.write("invalid: yaml: content:")
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
result = get_project(slug)
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestListProjects:
|
||||
"""Tests for list_projects function."""
|
||||
|
||||
def test_list_projects_returns_empty_list_when_no_projects(self, tmp_path):
|
||||
"""Test that list_projects returns empty list when no projects exist."""
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
projects = list_projects()
|
||||
assert projects == []
|
||||
|
||||
def test_list_projects_returns_all_projects(self, tmp_path, sample_project_data):
|
||||
"""Test that list_projects returns all existing projects."""
|
||||
project1_data = sample_project_data.copy()
|
||||
project1_data["slug"] = "project-1"
|
||||
project1_data["name"] = "Project 1"
|
||||
|
||||
project2_data = sample_project_data.copy()
|
||||
project2_data["slug"] = "project-2"
|
||||
project2_data["name"] = "Project 2"
|
||||
|
||||
for project_data in [project1_data, project2_data]:
|
||||
slug = project_data["slug"]
|
||||
project_dir = tmp_path / slug / "meta"
|
||||
project_dir.mkdir(parents=True)
|
||||
meta_path = project_dir / "project.yaml"
|
||||
with open(meta_path, "w") as f:
|
||||
yaml.safe_dump(project_data, f)
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
projects = list_projects()
|
||||
assert len(projects) == 2
|
||||
slugs = [p.slug for p in projects]
|
||||
assert "project-1" in slugs
|
||||
assert "project-2" in slugs
|
||||
|
||||
def test_list_projects_ignores_hidden_directories(self, tmp_path, sample_project_data):
|
||||
"""Test that list_projects ignores hidden directories."""
|
||||
slug = sample_project_data["slug"]
|
||||
project_dir = tmp_path / slug / "meta"
|
||||
project_dir.mkdir(parents=True)
|
||||
meta_path = project_dir / "project.yaml"
|
||||
with open(meta_path, "w") as f:
|
||||
yaml.safe_dump(sample_project_data, f)
|
||||
|
||||
hidden_dir = tmp_path / ".hidden"
|
||||
hidden_dir.mkdir()
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
projects = list_projects()
|
||||
assert len(projects) == 1
|
||||
assert projects[0].slug == slug
|
||||
|
||||
|
||||
class TestEnsureProjectStructure:
|
||||
"""Tests for ensure_project_structure function."""
|
||||
|
||||
def test_ensure_project_structure_creates_directories(self, tmp_path):
|
||||
"""Test that ensure_project_structure creates required directories."""
|
||||
slug = "test-project"
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
ensure_project_structure(slug)
|
||||
|
||||
project_root = tmp_path / slug
|
||||
assert (project_root / "sessions").exists()
|
||||
assert (project_root / "docs").exists()
|
||||
assert (project_root / "assets").exists()
|
||||
assert (project_root / "meta").exists()
|
||||
|
||||
def test_ensure_project_structure_is_idempotent(self, tmp_path):
|
||||
"""Test that ensure_project_structure can be called multiple times safely."""
|
||||
slug = "test-project"
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
ensure_project_structure(slug)
|
||||
ensure_project_structure(slug)
|
||||
|
||||
project_root = tmp_path / slug
|
||||
assert (project_root / "sessions").exists()
|
||||
assert (project_root / "docs").exists()
|
||||
|
||||
def test_ensure_project_structure_creates_nested_directories(self, tmp_path):
|
||||
"""Test that ensure_project_structure creates parent directories if needed."""
|
||||
slug = "nested/project"
|
||||
|
||||
with patch("tracker.services.project_service._PROJECTS_ROOT", tmp_path):
|
||||
ensure_project_structure(slug)
|
||||
|
||||
project_root = tmp_path / slug
|
||||
assert (project_root / "sessions").exists()
|
||||
Reference in New Issue
Block a user