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