Files
tracker-cli/tests/test_project_service.py
Daniel Arroyo 2735562b65 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.
2026-03-23 09:40:46 -03:00

210 lines
7.9 KiB
Python

"""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()