diff --git a/commands/add.py b/commands/add.py index 1b1a943..d29b6b3 100644 --- a/commands/add.py +++ b/commands/add.py @@ -1,22 +1,9 @@ """Add task command.""" import json -from pathlib import Path - -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" - - -def validate_description(description): - """Validate task description.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - if not description: - raise ValueError("Description cannot be empty") - if len(description) > 200: - raise ValueError("Description too long (max 200 chars)") - return description.strip() +from utils.paths import get_tasks_file +from utils.validation import validate_description def add_task(description): diff --git a/commands/done.py b/commands/done.py index c9dfd42..ec3c0c4 100644 --- a/commands/done.py +++ b/commands/done.py @@ -1,20 +1,9 @@ """Mark task done command.""" import json -from pathlib import Path - -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" - - -def validate_task_id(tasks, task_id): - """Validate task ID exists.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - if task_id < 1 or task_id > len(tasks): - raise ValueError(f"Invalid task ID: {task_id}") - return task_id +from utils.paths import get_tasks_file +from utils.validation import validate_task_id def mark_done(task_id): diff --git a/commands/list.py b/commands/list.py index 714315d..95a01a7 100644 --- a/commands/list.py +++ b/commands/list.py @@ -1,27 +1,14 @@ """List tasks command.""" import json -from pathlib import Path - -def get_tasks_file(): - """Get path to tasks file.""" - return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" - - -def validate_task_file(): - """Validate tasks file exists.""" - # NOTE: Validation logic scattered here - should be in utils (refactor bounty) - tasks_file = get_tasks_file() - if not tasks_file.exists(): - return [] - return tasks_file +from utils.paths import get_tasks_file +from utils.validation import validate_task_file def list_tasks(): """List all tasks.""" - # NOTE: No --json flag support yet (feature bounty) - tasks_file = validate_task_file() + tasks_file = validate_task_file(get_tasks_file()) if not tasks_file: print("No tasks yet!") return @@ -33,5 +20,5 @@ def list_tasks(): return for task in tasks: - status = "✓" if task["done"] else " " + status = "\u2713" if task["done"] else " " print(f"[{status}] {task['id']}. {task['description']}") diff --git a/test_task.py b/test_task.py index ba98e43..d6824ce 100644 --- a/test_task.py +++ b/test_task.py @@ -2,9 +2,22 @@ import json import pytest +import subprocess from pathlib import Path -from commands.add import add_task, validate_description -from commands.done import validate_task_id +from commands.add import add_task +from commands.list import list_tasks +from commands.done import mark_done +from utils.validation import validate_description, validate_task_id, validate_task_file +from utils.paths import get_tasks_file + + +def _patch_tasks_file(monkeypatch, tmp_path): + """Patch get_tasks_file in all modules that import it.""" + target = tmp_path / "tasks.json" + monkeypatch.setattr("commands.add.get_tasks_file", lambda: target) + monkeypatch.setattr("commands.list.get_tasks_file", lambda: target) + monkeypatch.setattr("commands.done.get_tasks_file", lambda: target) + return target def test_validate_description(): @@ -28,3 +41,61 @@ def test_validate_task_id(): with pytest.raises(ValueError): validate_task_id(tasks, 99) + + +def test_validate_task_file(tmp_path): + """Test task file validation.""" + # Non-existent file returns empty list + missing = tmp_path / "nonexistent.json" + assert validate_task_file(missing) == [] + + # Existing file returns the path + existing = tmp_path / "tasks.json" + existing.write_text("[]") + assert validate_task_file(existing) == existing + + +def test_get_tasks_file(): + """Test shared path helper returns expected path.""" + result = get_tasks_file() + assert result == Path.home() / ".local" / "share" / "task-cli" / "tasks.json" + + +def test_add_task(capsys, tmp_path, monkeypatch): + """Test add command produces correct output.""" + _patch_tasks_file(monkeypatch, tmp_path) + add_task("Test task") + output = capsys.readouterr().out.strip() + assert "Added task 1: Test task" == output + + +def test_list_empty(capsys, tmp_path, monkeypatch): + """Test list command with no tasks.""" + _patch_tasks_file(monkeypatch, tmp_path) + list_tasks() + output = capsys.readouterr().out.strip() + assert "No tasks yet!" == output + + +def test_list_tasks(capsys, tmp_path, monkeypatch): + """Test list command shows tasks.""" + _patch_tasks_file(monkeypatch, tmp_path) + add_task("First task") + add_task("Second task") + list_tasks() + output = capsys.readouterr().out.strip() + assert "1. First task" in output + assert "2. Second task" in output + + +def test_mark_done(capsys, tmp_path, monkeypatch): + """Test done command marks task complete.""" + _patch_tasks_file(monkeypatch, tmp_path) + add_task("Task to complete") + mark_done(1) + lines = capsys.readouterr().out.strip().split("\n") + assert "Marked task 1 as done: Task to complete" == lines[-1] + + # Verify the file was updated + tasks = json.loads((tmp_path / "tasks.json").read_text()) + assert tasks[0]["done"] is True diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..3857fda --- /dev/null +++ b/utils/__init__.py @@ -0,0 +1 @@ +"""Shared utilities for task CLI.""" diff --git a/utils/paths.py b/utils/paths.py new file mode 100644 index 0000000..ec29514 --- /dev/null +++ b/utils/paths.py @@ -0,0 +1,8 @@ +"""Shared path helpers for task CLI.""" + +from pathlib import Path + + +def get_tasks_file(): + """Get path to tasks file.""" + return Path.home() / ".local" / "share" / "task-cli" / "tasks.json" diff --git a/utils/validation.py b/utils/validation.py new file mode 100644 index 0000000..d69157a --- /dev/null +++ b/utils/validation.py @@ -0,0 +1,52 @@ +"""Shared validation functions for task CLI.""" + + +def validate_description(description): + """Validate task description. + + Args: + description: Raw task description string. + + Returns: + Stripped description string. + + Raises: + ValueError: If description is empty or exceeds 200 characters. + """ + if not description: + raise ValueError("Description cannot be empty") + if len(description) > 200: + raise ValueError("Description too long (max 200 chars)") + return description.strip() + + +def validate_task_id(tasks, task_id): + """Validate task ID exists in the task list. + + Args: + tasks: List of task dicts. + task_id: Integer task ID to validate. + + Returns: + Validated task ID. + + Raises: + ValueError: If task ID is out of range. + """ + if task_id < 1 or task_id > len(tasks): + raise ValueError(f"Invalid task ID: {task_id}") + return task_id + + +def validate_task_file(tasks_file): + """Validate tasks file exists and return it, or empty list if missing. + + Args: + tasks_file: Path object to the tasks JSON file. + + Returns: + The tasks_file Path if it exists, or an empty list if not. + """ + if not tasks_file.exists(): + return [] + return tasks_file