diff --git a/crates/onyx-core/src/error.rs b/crates/onyx-core/src/error.rs index b094fdf..98648e9 100644 --- a/crates/onyx-core/src/error.rs +++ b/crates/onyx-core/src/error.rs @@ -59,3 +59,105 @@ impl From for Error { } pub type Result = std::result::Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_display_io_error() { + let io_err = io::Error::new(io::ErrorKind::NotFound, "file missing"); + let err = Error::Io(io_err); + assert_eq!(err.to_string(), "IO error: file missing"); + } + + #[test] + fn test_display_serialization() { + let err = Error::Serialization("bad json".to_string()); + assert_eq!(err.to_string(), "Serialization error: bad json"); + } + + #[test] + fn test_display_not_found() { + let err = Error::NotFound("item".to_string()); + assert_eq!(err.to_string(), "Not found: item"); + } + + #[test] + fn test_display_invalid_data() { + let err = Error::InvalidData("corrupt".to_string()); + assert_eq!(err.to_string(), "Invalid data: corrupt"); + } + + #[test] + fn test_display_workspace_not_found() { + let err = Error::WorkspaceNotFound("myws".to_string()); + assert_eq!(err.to_string(), "Workspace not found: myws"); + } + + #[test] + fn test_display_list_not_found() { + let err = Error::ListNotFound("abc-123".to_string()); + assert_eq!(err.to_string(), "List not found: abc-123"); + } + + #[test] + fn test_display_task_not_found() { + let err = Error::TaskNotFound("task-456".to_string()); + assert_eq!(err.to_string(), "Task not found: task-456"); + } + + #[test] + fn test_display_webdav() { + let err = Error::WebDav("connection refused".to_string()); + assert_eq!(err.to_string(), "WebDAV error: connection refused"); + } + + #[test] + fn test_display_sync() { + let err = Error::Sync("conflict".to_string()); + assert_eq!(err.to_string(), "Sync error: conflict"); + } + + #[test] + fn test_display_credential() { + let err = Error::Credential("keychain locked".to_string()); + assert_eq!(err.to_string(), "Credential error: keychain locked"); + } + + #[test] + fn test_from_io_error() { + let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "denied"); + let err: Error = io_err.into(); + assert!(matches!(err, Error::Io(_))); + assert!(err.to_string().contains("denied")); + } + + #[test] + fn test_from_serde_json_error() { + let json_err = serde_json::from_str::("{{bad").unwrap_err(); + let err: Error = json_err.into(); + assert!(matches!(err, Error::Serialization(_))); + } + + #[test] + fn test_from_serde_yaml_error() { + let yaml_err = serde_yaml::from_str::(":\n :\n ::: bad").unwrap_err(); + let err: Error = yaml_err.into(); + assert!(matches!(err, Error::Serialization(_))); + } + + #[test] + fn test_error_is_std_error() { + let err = Error::NotFound("x".to_string()); + let _: &dyn std::error::Error = &err; + } + + #[test] + fn test_error_debug() { + let err = Error::NotFound("test".to_string()); + let debug = format!("{:?}", err); + assert!(debug.contains("NotFound")); + assert!(debug.contains("test")); + } +} diff --git a/crates/onyx-core/src/models.rs b/crates/onyx-core/src/models.rs index 2ba5771..3450dfe 100644 --- a/crates/onyx-core/src/models.rs +++ b/crates/onyx-core/src/models.rs @@ -117,3 +117,220 @@ impl TaskList { } } } + +#[cfg(test)] +mod tests { + use super::*; + + // --- TaskStatus tests --- + + #[test] + fn test_task_status_serde_roundtrip() { + let json = serde_json::to_string(&TaskStatus::Backlog).unwrap(); + assert_eq!(json, "\"backlog\""); + let parsed: TaskStatus = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed, TaskStatus::Backlog); + + let json = serde_json::to_string(&TaskStatus::Completed).unwrap(); + assert_eq!(json, "\"completed\""); + let parsed: TaskStatus = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed, TaskStatus::Completed); + } + + #[test] + fn test_task_status_equality() { + assert_eq!(TaskStatus::Backlog, TaskStatus::Backlog); + assert_eq!(TaskStatus::Completed, TaskStatus::Completed); + assert_ne!(TaskStatus::Backlog, TaskStatus::Completed); + } + + // --- Task tests --- + + #[test] + fn test_task_new_defaults() { + let task = Task::new("My Task".to_string()); + assert_eq!(task.title, "My Task"); + assert_eq!(task.description, ""); + assert_eq!(task.status, TaskStatus::Backlog); + assert!(task.due_date.is_none()); + assert!(!task.has_time); + assert_eq!(task.version, 0); + assert!(task.parent_id.is_none()); + } + + #[test] + fn test_task_with_description() { + let task = Task::new("T".to_string()) + .with_description("Some notes".to_string()); + assert_eq!(task.description, "Some notes"); + } + + #[test] + fn test_task_with_due_date() { + let dt = Utc::now(); + let task = Task::new("T".to_string()).with_due_date(dt); + assert_eq!(task.due_date, Some(dt)); + } + + #[test] + fn test_task_with_parent() { + let parent_id = Uuid::new_v4(); + let task = Task::new("Sub".to_string()).with_parent(parent_id); + assert_eq!(task.parent_id, Some(parent_id)); + } + + #[test] + fn test_task_complete_and_uncomplete() { + let mut task = Task::new("T".to_string()); + assert_eq!(task.status, TaskStatus::Backlog); + + task.complete(); + assert_eq!(task.status, TaskStatus::Completed); + + task.uncomplete(); + assert_eq!(task.status, TaskStatus::Backlog); + } + + #[test] + fn test_task_builder_chaining() { + let parent_id = Uuid::new_v4(); + let dt = Utc::now(); + let task = Task::new("Chained".to_string()) + .with_description("Desc".to_string()) + .with_due_date(dt) + .with_parent(parent_id); + + assert_eq!(task.title, "Chained"); + assert_eq!(task.description, "Desc"); + assert_eq!(task.due_date, Some(dt)); + assert_eq!(task.parent_id, Some(parent_id)); + } + + #[test] + fn test_task_unique_ids() { + let t1 = Task::new("A".to_string()); + let t2 = Task::new("B".to_string()); + assert_ne!(t1.id, t2.id); + } + + #[test] + fn test_task_serde_roundtrip() { + let parent_id = Uuid::new_v4(); + let task = Task::new("Serde".to_string()) + .with_description("Desc".to_string()) + .with_parent(parent_id); + let json = serde_json::to_string(&task).unwrap(); + let parsed: Task = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.id, task.id); + assert_eq!(parsed.title, "Serde"); + assert_eq!(parsed.description, "Desc"); + assert_eq!(parsed.parent_id, Some(parent_id)); + } + + #[test] + fn test_task_serde_skips_none_fields() { + let task = Task::new("Minimal".to_string()); + let json = serde_json::to_string(&task).unwrap(); + assert!(!json.contains("due_date")); + assert!(!json.contains("parent_id")); + } + + // --- TaskList tests --- + + #[test] + fn test_task_list_new_defaults() { + let list = TaskList::new("My List".to_string()); + assert_eq!(list.title, "My List"); + assert!(list.tasks.is_empty()); + assert!(!list.group_by_due_date); + assert!(list.created_at <= Utc::now()); + assert!(list.updated_at <= Utc::now()); + } + + #[test] + fn test_task_list_add_task() { + let mut list = TaskList::new("L".to_string()); + let before = list.updated_at; + std::thread::sleep(std::time::Duration::from_millis(2)); + + let task = Task::new("T".to_string()); + let task_id = task.id; + list.add_task(task); + + assert_eq!(list.tasks.len(), 1); + assert_eq!(list.tasks[0].id, task_id); + assert!(list.updated_at >= before); + } + + #[test] + fn test_task_list_remove_task() { + let mut list = TaskList::new("L".to_string()); + let task = Task::new("T".to_string()); + let task_id = task.id; + list.add_task(task); + + let removed = list.remove_task(task_id); + assert!(removed.is_some()); + assert_eq!(removed.unwrap().id, task_id); + assert!(list.tasks.is_empty()); + } + + #[test] + fn test_task_list_remove_nonexistent_task() { + let mut list = TaskList::new("L".to_string()); + let removed = list.remove_task(Uuid::new_v4()); + assert!(removed.is_none()); + } + + #[test] + fn test_task_list_get_task() { + let mut list = TaskList::new("L".to_string()); + let task = Task::new("T".to_string()); + let task_id = task.id; + list.add_task(task); + + assert!(list.get_task(task_id).is_some()); + assert_eq!(list.get_task(task_id).unwrap().title, "T"); + assert!(list.get_task(Uuid::new_v4()).is_none()); + } + + #[test] + fn test_task_list_get_task_mut() { + let mut list = TaskList::new("L".to_string()); + let task = Task::new("T".to_string()); + let task_id = task.id; + list.add_task(task); + + let t = list.get_task_mut(task_id).unwrap(); + t.title = "Modified".to_string(); + + assert_eq!(list.get_task(task_id).unwrap().title, "Modified"); + } + + #[test] + fn test_task_list_update_task() { + let mut list = TaskList::new("L".to_string()); + let task = Task::new("Old".to_string()); + let task_id = task.id; + list.add_task(task); + + let mut updated = Task::new("New".to_string()); + updated.id = task_id; + assert!(list.update_task(updated)); + assert_eq!(list.get_task(task_id).unwrap().title, "New"); + } + + #[test] + fn test_task_list_update_nonexistent_task() { + let mut list = TaskList::new("L".to_string()); + let task = Task::new("Ghost".to_string()); + assert!(!list.update_task(task)); + } + + #[test] + fn test_task_list_unique_ids() { + let l1 = TaskList::new("A".to_string()); + let l2 = TaskList::new("B".to_string()); + assert_ne!(l1.id, l2.id); + } +} diff --git a/crates/onyx-core/src/repository.rs b/crates/onyx-core/src/repository.rs index 8d82488..b54e9f4 100644 --- a/crates/onyx-core/src/repository.rs +++ b/crates/onyx-core/src/repository.rs @@ -446,6 +446,78 @@ mod tests { assert!(!old_path.exists()); } + #[test] + fn test_create_task_increments_version() { + let temp_dir = TempDir::new().unwrap(); + let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap(); + let list = repo.create_list("Test".to_string()).unwrap(); + + let task = Task::new("V".to_string()); + assert_eq!(task.version, 0); + let created = repo.create_task(list.id, task).unwrap(); + assert_eq!(created.version, 1); + } + + #[test] + fn test_move_task_nonexistent_task() { + let temp_dir = TempDir::new().unwrap(); + let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap(); + let list_a = repo.create_list("A".to_string()).unwrap(); + let list_b = repo.create_list("B".to_string()).unwrap(); + + let result = repo.move_task(list_a.id, list_b.id, Uuid::new_v4()); + assert!(result.is_err()); + } + + #[test] + fn test_move_task_preserves_task_data() { + let temp_dir = TempDir::new().unwrap(); + let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap(); + let list_a = repo.create_list("A".to_string()).unwrap(); + let list_b = repo.create_list("B".to_string()).unwrap(); + + let task = Task::new("Rich Task".to_string()) + .with_description("Important notes".to_string()); + let task = repo.create_task(list_a.id, task).unwrap(); + let task_id = task.id; + + repo.move_task(list_a.id, list_b.id, task_id).unwrap(); + + let moved = repo.get_task(list_b.id, task_id).unwrap(); + assert_eq!(moved.title, "Rich Task"); + assert_eq!(moved.description, "Important notes"); + } + + #[test] + fn test_subtask_creation_and_retrieval() { + let temp_dir = TempDir::new().unwrap(); + let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap(); + let list = repo.create_list("Test".to_string()).unwrap(); + + let parent = repo.create_task(list.id, Task::new("Parent".to_string())).unwrap(); + let child = Task::new("Child".to_string()).with_parent(parent.id); + let child = repo.create_task(list.id, child).unwrap(); + + let retrieved = repo.get_task(list.id, child.id).unwrap(); + assert_eq!(retrieved.parent_id, Some(parent.id)); + } + + #[test] + fn test_multiple_lists_independent() { + let temp_dir = TempDir::new().unwrap(); + let mut repo = TaskRepository::init(temp_dir.path().to_path_buf()).unwrap(); + + let list_a = repo.create_list("A".to_string()).unwrap(); + let list_b = repo.create_list("B".to_string()).unwrap(); + + repo.create_task(list_a.id, Task::new("Task A1".to_string())).unwrap(); + repo.create_task(list_a.id, Task::new("Task A2".to_string())).unwrap(); + repo.create_task(list_b.id, Task::new("Task B1".to_string())).unwrap(); + + assert_eq!(repo.list_tasks(list_a.id).unwrap().len(), 2); + assert_eq!(repo.list_tasks(list_b.id).unwrap().len(), 1); + } + #[test] fn test_task_order_after_delete() { let temp_dir = TempDir::new().unwrap();