Add tests for models.rs, error.rs, and repository.rs edge cases

Addresses critical test coverage gaps in onyx-core:
- models.rs: 20 tests for Task, TaskList, TaskStatus (builder, serde, CRUD)
- error.rs: 15 tests for Display impls and From conversions
- repository.rs: 5 new tests for version increment, move_task errors,
  subtask creation, multi-list independence

Total: 122 -> 162 tests passing.

https://claude.ai/code/session_01XiAFtZ7CAvm9FhrNCwAwsr
This commit is contained in:
Claude 2026-04-06 11:50:52 +00:00
parent 36fa591799
commit ba7ac15d0c
No known key found for this signature in database
3 changed files with 391 additions and 0 deletions

View file

@ -59,3 +59,105 @@ impl From<reqwest::Error> for Error {
}
pub type Result<T> = std::result::Result<T, Error>;
#[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::<serde_json::Value>("{{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::<serde_yaml::Value>(":\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"));
}
}

View file

@ -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);
}
}

View file

@ -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();