"""
Tests for the task importer
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from src.importers.task_importer import TaskImporter
from src.importers.markdown_parser import MarkdownTask
@pytest.mark.unit
class TestTaskImporter:
"""Test the TaskImporter class"""
def test_importer_initialization_with_token(self, mock_env_token):
"""Test importer initialization with environment token"""
with patch('src.importers.task_importer.TickTickAPI'):
importer = TaskImporter()
assert importer.access_token == 'test_token_123'
assert importer.api is not None
def test_importer_initialization_without_token(self, monkeypatch):
"""Test importer initialization without token"""
monkeypatch.delenv('TICKTICK_ACCESS_TOKEN', raising=False)
importer = TaskImporter()
assert importer.access_token is None
assert importer.api is None
def test_importer_initialization_with_custom_token(self):
"""Test importer initialization with custom token"""
with patch('src.importers.task_importer.TickTickAPI'):
importer = TaskImporter(access_token='custom_token')
assert importer.access_token == 'custom_token'
def test_authenticate_success(self, mock_env_token, mock_ticktick_api):
"""Test successful authentication"""
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
result = importer.authenticate()
assert result is True
mock_ticktick_api.get_user_info.assert_called_once()
def test_authenticate_no_token(self, monkeypatch, capsys):
"""Test authentication without token"""
monkeypatch.delenv('TICKTICK_ACCESS_TOKEN', raising=False)
importer = TaskImporter()
result = importer.authenticate()
assert result is False
captured = capsys.readouterr()
assert "No access token found" in captured.out
def test_authenticate_invalid_token(self, mock_env_token):
"""Test authentication with invalid token"""
mock_api = MagicMock()
mock_api.get_user_info.return_value = None
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_api):
importer = TaskImporter()
result = importer.authenticate()
assert result is False
def test_get_projects(self, mock_env_token, mock_ticktick_api):
"""Test getting projects"""
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
projects = importer.get_projects()
assert len(projects) == 3
assert projects[0]['name'] == 'Work Tasks'
def test_get_projects_cached(self, mock_env_token, mock_ticktick_api):
"""Test that projects are cached"""
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
# First call
projects1 = importer.get_projects()
# Second call should use cache
projects2 = importer.get_projects()
assert projects1 == projects2
# Should only call API once
assert mock_ticktick_api.get_projects.call_count == 1
def test_get_projects_refresh(self, mock_env_token, mock_ticktick_api):
"""Test refreshing projects cache"""
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
importer.get_projects()
importer.get_projects(refresh=True)
# Should call API twice with refresh
assert mock_ticktick_api.get_projects.call_count == 2
def test_find_project_id(self, mock_env_token, mock_ticktick_api):
"""Test finding project ID by name"""
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
project_id = importer.find_project_id("Work Tasks")
assert project_id == 'proj1'
def test_find_project_id_case_insensitive(self, mock_env_token, mock_ticktick_api):
"""Test finding project ID is case insensitive"""
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
project_id = importer.find_project_id("work tasks")
assert project_id == 'proj1'
def test_find_project_id_partial_match(self, mock_env_token, mock_ticktick_api):
"""Test finding project ID with partial name"""
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
project_id = importer.find_project_id("Work")
assert project_id == 'proj1'
def test_find_project_id_not_found(self, mock_env_token, mock_ticktick_api):
"""Test finding non-existent project"""
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
project_id = importer.find_project_id("Nonexistent")
assert project_id is None
def test_import_task_success(self, mock_env_token, mock_ticktick_api):
"""Test successfully importing a task"""
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
task = MarkdownTask("Test Task")
task.tags = ["test"]
task.content = ["Description"]
result = importer._import_task(task, "Work Tasks")
assert result['status'] == 'created'
assert 'task_id' in result
mock_ticktick_api.create_task.assert_called_once()
def test_import_task_completed_skipped(self, mock_env_token, mock_ticktick_api):
"""Test that completed tasks are skipped"""
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
task = MarkdownTask("Completed Task")
task.completed = True
result = importer._import_task(task, None)
assert result['status'] == 'skipped'
assert result['reason'] == 'already completed'
mock_ticktick_api.create_task.assert_not_called()
def test_import_task_project_not_found(self, mock_env_token, mock_ticktick_api):
"""Test importing task with non-existent project"""
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
task = MarkdownTask("Test Task")
task.project = "Nonexistent Project"
result = importer._import_task(task, None)
assert result['status'] == 'failed'
assert 'not found' in result['error'].lower()
def test_import_task_api_error(self, mock_env_token):
"""Test handling API error during import"""
mock_api = MagicMock()
mock_api.get_projects.return_value = []
mock_api.create_task.side_effect = Exception("API Error")
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_api):
importer = TaskImporter()
task = MarkdownTask("Test Task")
result = importer._import_task(task, None)
assert result['status'] == 'failed'
assert 'API Error' in result['error']
def test_import_from_file_success(self, tmp_path, mock_env_token, mock_ticktick_api):
"""Test importing tasks from a file"""
md_file = tmp_path / "test.md"
md_file.write_text("""- [ ] Task 1
Tags: test
- [ ] Task 2
Tags: test""")
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
result = importer.import_from_file(str(md_file), dry_run=True)
assert result['dry_run'] is True
assert result['tasks'] == 2
def test_import_from_file_not_authenticated(self, tmp_path, monkeypatch):
"""Test importing without authentication"""
monkeypatch.delenv('TICKTICK_ACCESS_TOKEN', raising=False)
importer = TaskImporter()
result = importer.import_from_file("test.md")
assert 'error' in result
assert result['error'] == 'Not authenticated'
def test_import_from_file_no_tasks(self, tmp_path, mock_env_token, mock_ticktick_api):
"""Test importing file with no tasks"""
md_file = tmp_path / "empty.md"
md_file.write_text("# Empty file\n\nNo tasks here")
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
result = importer.import_from_file(str(md_file))
assert 'error' in result
assert 'No tasks found' in result['error']
def test_import_from_file_actual_import(self, tmp_path, mock_env_token, mock_ticktick_api):
"""Test actually importing tasks (not dry run)"""
md_file = tmp_path / "test.md"
md_file.write_text("""- [ ] Task 1
Tags: test
- [x] Completed Task
- [ ] Task 2""")
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
result = importer.import_from_file(str(md_file), dry_run=False)
assert result['total'] == 3
assert result['created'] == 2 # Only uncompleted tasks
assert result['skipped'] == 1 # Completed task
assert result['failed'] == 0
def test_import_with_default_project(self, tmp_path, mock_env_token, mock_ticktick_api):
"""Test importing with default project"""
md_file = tmp_path / "test.md"
md_file.write_text("- [ ] Task without project")
with patch('src.importers.task_importer.TickTickAPI', return_value=mock_ticktick_api):
importer = TaskImporter()
result = importer.import_from_file(str(md_file), default_project="Work Tasks", dry_run=False)
assert result['created'] == 1
# Verify create_task was called with project_id
call_args = mock_ticktick_api.create_task.call_args
assert call_args.kwargs['project_id'] == 'proj1'