diff --git a/README.md b/README.md index 91da9a7..508fc9d 100644 --- a/README.md +++ b/README.md @@ -33,4 +33,4 @@ python -m pytest test_task.py ## Configuration -Copy `config.yaml.example` to `~/.config/task-cli/config.yaml` and customize. +A default configuration file is automatically created at `~/.config/task-cli/config.yaml` on first run if one does not already exist. To customize, edit this file or copy `config.yaml.example` as a starting point. diff --git a/task.py b/task.py index 53cc8ed..f5d78cd 100644 --- a/task.py +++ b/task.py @@ -9,13 +9,43 @@ from commands.list import list_tasks from commands.done import mark_done +DEFAULT_CONFIG = """\ +# Task CLI configuration +# See config.yaml.example for all options + +# Task storage settings +storage: + format: json + max_tasks: 1000 + +# Display settings +display: + color: true + unicode: true +""" + + +def get_config_path(): + """Get path to config file.""" + return Path.home() / ".config" / "task-cli" / "config.yaml" + def load_config(): - """Load configuration from file.""" - config_path = Path.home() / ".config" / "task-cli" / "config.yaml" - # NOTE: This will crash if config doesn't exist - known bug for bounty testing - with open(config_path) as f: - return f.read() + """Load configuration from file. + + If the config file does not exist, creates a default one and returns it. + If the config directory does not exist, creates it first. + + Returns: + The configuration file contents as a string. + """ + config_path = get_config_path() + + if not config_path.exists(): + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text(DEFAULT_CONFIG) + + return config_path.read_text() def main(): @@ -35,6 +65,9 @@ def main(): args = parser.parse_args() + # Load config gracefully — creates default if missing + config = load_config() + if args.command == "add": add_task(args.description) elif args.command == "list": diff --git a/test_task.py b/test_task.py index ba98e43..165ace0 100644 --- a/test_task.py +++ b/test_task.py @@ -2,9 +2,11 @@ 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 task import load_config, get_config_path, DEFAULT_CONFIG def test_validate_description(): @@ -28,3 +30,53 @@ def test_validate_task_id(): with pytest.raises(ValueError): validate_task_id(tasks, 99) + + +# ── Config file tests ──────────────────────────────────────────────────── + + +def test_load_config_creates_default_when_missing(tmp_path, monkeypatch): + """Test that load_config creates a default config when file is missing.""" + config_path = tmp_path / "config" / "task-cli" / "config.yaml" + monkeypatch.setattr("task.get_config_path", lambda: config_path) + + result = load_config() + + assert config_path.exists(), "Config file should be created" + assert result == DEFAULT_CONFIG + assert "storage:" in result + assert "display:" in result + + +def test_load_config_reads_existing(tmp_path, monkeypatch): + """Test that load_config reads an existing config without overwriting.""" + config_path = tmp_path / "config.yaml" + custom_config = "custom: true\n" + config_path.write_text(custom_config) + monkeypatch.setattr("task.get_config_path", lambda: config_path) + + result = load_config() + + assert result == custom_config + + +def test_load_config_creates_parent_dirs(tmp_path, monkeypatch): + """Test that load_config creates parent directories if needed.""" + config_path = tmp_path / "deep" / "nested" / "dir" / "config.yaml" + monkeypatch.setattr("task.get_config_path", lambda: config_path) + + load_config() + + assert config_path.exists() + assert config_path.parent.is_dir() + + +def test_cli_no_crash_without_config(): + """Test that CLI doesn't crash when config file is missing.""" + result = subprocess.run( + ["python3", "task.py", "list"], + capture_output=True, text=True, cwd=str(Path(__file__).parent), + ) + # Should not crash with FileNotFoundError + assert result.returncode == 0 + assert "FileNotFoundError" not in result.stderr