from __future__ import annotations from datetime import UTC, datetime, timedelta from uuid import uuid4 import pytest from pydantic import ValidationError from app.storage import StoredTask from app.storage.repository import JsonFileTaskRepository, StorageTaskNotFoundError def make_task( *, title: str = "Task", started: bool = False, done: bool = False, ) -> StoredTask: now = datetime.now(UTC) started_at = now + timedelta(seconds=1) if started else None done_at = now + timedelta(seconds=2) if done else None return StoredTask( id=uuid4(), title=title, created_at=now, started_at=started_at, done_at=done_at, ) def make_repo(tmp_path) -> JsonFileTaskRepository: return JsonFileTaskRepository(tmp_path / "tasks.json") def test_create_task_in_empty_storage(tmp_path): repo = make_repo(tmp_path) task = make_task(title="First task") created = repo.create_task(task) tasks = repo.list_tasks() assert created == task assert len(tasks) == 1 assert tasks[0] == task def test_create_task_sequentially_preserves_insertion_order(tmp_path): repo = make_repo(tmp_path) task_1 = make_task(title="First") task_2 = make_task(title="Second") repo.create_task(task_1) repo.create_task(task_2) tasks = repo.list_tasks() assert [task.id for task in tasks] == [task_1.id, task_2.id] def test_create_task_invalid_model_rejected_before_repository_call(tmp_path): repo = make_repo(tmp_path) with pytest.raises(ValidationError): StoredTask( id=uuid4(), title="", created_at=datetime.now(UTC), ) assert repo.list_tasks() == [] def test_list_tasks_returns_empty_list_for_fresh_storage(tmp_path): repo = make_repo(tmp_path) assert repo.list_tasks() == [] def test_list_tasks_returns_persisted_tasks(tmp_path): repo = make_repo(tmp_path) task_1 = make_task(title="One") task_2 = make_task(title="Two") repo.create_task(task_1) repo.create_task(task_2) tasks = repo.list_tasks() assert tasks == [task_1, task_2] def test_get_task_returns_existing_task(tmp_path): repo = make_repo(tmp_path) task = make_task(title="Lookup me") repo.create_task(task) found = repo.get_task(task.id) assert found == task def test_get_task_returns_none_for_missing_id(tmp_path): repo = make_repo(tmp_path) repo.create_task(make_task(title="Other task")) found = repo.get_task(uuid4()) assert found is None def test_get_task_with_wrong_runtime_type_returns_none_and_does_not_change_storage(tmp_path): repo = make_repo(tmp_path) task = make_task(title="Type safety") repo.create_task(task) found = repo.get_task("not-a-uuid") # type: ignore[arg-type] assert found is None assert repo.list_tasks() == [task] def test_update_task_replaces_existing_fields(tmp_path): repo = make_repo(tmp_path) original = make_task(title="Old title") repo.create_task(original) updated = StoredTask( id=original.id, title="New title", created_at=original.created_at, started_at=original.started_at, done_at=original.done_at, ) result = repo.update_task(updated) assert result == updated assert repo.get_task(original.id) == updated def test_update_task_persists_transition_related_fields(tmp_path): repo = make_repo(tmp_path) original = make_task(title="Workflow") repo.create_task(original) now = datetime.now(UTC) updated = StoredTask( id=original.id, title=original.title, created_at=original.created_at, started_at=now, done_at=now + timedelta(minutes=5), ) repo.update_task(updated) reloaded = repo.get_task(original.id) assert reloaded is not None assert reloaded.started_at == updated.started_at assert reloaded.done_at == updated.done_at def test_update_task_raises_for_missing_task(tmp_path): repo = make_repo(tmp_path) missing = make_task(title="Missing") with pytest.raises(StorageTaskNotFoundError): repo.update_task(missing) assert repo.list_tasks() == [] def test_delete_task_removes_existing_task(tmp_path): repo = make_repo(tmp_path) task = make_task(title="Delete me") repo.create_task(task) deleted = repo.delete_task(task.id) assert deleted is True assert repo.list_tasks() == [] def test_delete_task_only_affects_requested_task(tmp_path): repo = make_repo(tmp_path) task_1 = make_task(title="Keep me") task_2 = make_task(title="Delete me") repo.create_task(task_1) repo.create_task(task_2) deleted = repo.delete_task(task_2.id) assert deleted is True assert repo.list_tasks() == [task_1] def test_delete_task_returns_false_for_missing_task(tmp_path): repo = make_repo(tmp_path) task = make_task(title="Existing") repo.create_task(task) deleted = repo.delete_task(uuid4()) assert deleted is False assert repo.list_tasks() == [task]