//! Integration tests for SqliteTaskRepository. mod common; use chrono::{Duration, Utc}; use goingson_core::{NewTask, Priority, Recurrence, TaskRepository, TaskStatus, UpdateTask}; use goingson_db_sqlite::SqliteTaskRepository; #[tokio::test] async fn test_create_and_get_task() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let new_task = NewTask::builder("Test task") .priority(Priority::Medium) .tag("test") .build(); let created = repo.create(user_id, new_task).await.expect("Failed to create task"); assert_eq!(created.description, "Test task"); assert_eq!(created.status, TaskStatus::Pending); assert_eq!(created.priority, Priority::Medium); assert_eq!(created.tags, vec!["test"]); let fetched = repo.get_by_id(created.id, user_id).await.expect("Failed to get task"); assert!(fetched.is_some()); assert_eq!(fetched.unwrap().id, created.id); } #[tokio::test] async fn test_task_status_transitions() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let new_task = NewTask::builder("Status test") .priority(Priority::High) .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create"); assert_eq!(task.status, TaskStatus::Pending); // Start the task let started = repo.start(task.id, user_id).await.expect("Failed to start"); assert!(started); // Verify status changed let fetched = repo.get_by_id(task.id, user_id).await.expect("Failed to get"); assert_eq!(fetched.unwrap().status, TaskStatus::Started); // Complete the task let completed = repo.complete(task.id, user_id).await.expect("Failed to complete"); assert!(completed.is_some()); // Verify status changed let fetched = repo.get_by_id(task.id, user_id).await.expect("Failed to get"); assert_eq!(fetched.unwrap().status, TaskStatus::Completed); } #[tokio::test] async fn test_task_snooze() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let new_task = NewTask::builder("Snooze test") .priority(Priority::Low) .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create"); let until = Utc::now() + Duration::hours(2); let snoozed = repo.snooze(task.id, user_id, until).await.expect("Failed to snooze"); assert!(snoozed.is_some()); assert!(snoozed.as_ref().unwrap().snoozed_until.is_some()); // Should appear in snoozed list let snoozed_tasks = repo.list_snoozed(user_id).await.expect("Failed to list snoozed"); assert_eq!(snoozed_tasks.len(), 1); // Unsnooze let unsnoozed = repo.unsnooze(task.id, user_id).await.expect("Failed to unsnooze"); assert!(unsnoozed.is_some()); assert!(unsnoozed.unwrap().snoozed_until.is_none()); } #[tokio::test] async fn test_task_waiting_for_response() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let new_task = NewTask::builder("Waiting test") .priority(Priority::Medium) .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create"); let expected = Utc::now() + Duration::days(3); let waiting = repo.mark_waiting(task.id, user_id, Some(expected)).await.expect("Failed to mark waiting"); assert!(waiting.is_some()); assert!(waiting.as_ref().unwrap().waiting_for_response); // Should appear in waiting list let waiting_tasks = repo.list_waiting(user_id).await.expect("Failed to list waiting"); assert_eq!(waiting_tasks.len(), 1); // Clear waiting let cleared = repo.clear_waiting(task.id, user_id).await.expect("Failed to clear"); assert!(cleared.is_some()); assert!(!cleared.unwrap().waiting_for_response); } #[tokio::test] async fn test_task_with_due_date() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let due = Utc::now() + Duration::days(1); let new_task = NewTask::builder("Due tomorrow") .priority(Priority::High) .due(due) .tag("urgent") .urgency(8.5) // High priority task with urgency set .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create"); assert!(task.due.is_some()); // Urgency should be stored as provided assert!((task.urgency - 8.5).abs() < 0.01); } #[tokio::test] async fn test_delete_task() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let new_task = NewTask::builder("To delete") .priority(Priority::Low) .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create"); let deleted = repo.delete(task.id, user_id).await.expect("Failed to delete"); assert!(deleted); // Task should not appear in normal list (deleted tasks are filtered out) let tasks = repo.list_all(user_id).await.expect("Failed to list"); assert!(tasks.iter().all(|t| t.id != task.id)); } #[tokio::test] async fn test_task_urgency_ordering() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); // Create low urgency task let low = NewTask::builder("Low urgency") .priority(Priority::Low) .urgency(2.0) .build(); repo.create(user_id, low).await.expect("Failed to create"); // Create high urgency task let high = NewTask::builder("High urgency") .priority(Priority::High) .urgency(8.0) .build(); repo.create(user_id, high).await.expect("Failed to create"); // Create highest urgency task (overdue) let overdue = NewTask::builder("Overdue task") .priority(Priority::Medium) .due(Utc::now() - Duration::days(1)) .urgency(15.0) // Overdue penalty applied .build(); repo.create(user_id, overdue).await.expect("Failed to create"); let tasks = repo.list_all(user_id).await.expect("Failed to list"); assert_eq!(tasks.len(), 3); // Tasks should be ordered by urgency descending assert!(tasks[0].urgency >= tasks[1].urgency); assert!(tasks[1].urgency >= tasks[2].urgency); // Verify the highest urgency task is first assert_eq!(tasks[0].description, "Overdue task"); } #[tokio::test] async fn test_complete_sets_completed_at() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let new_task = NewTask::builder("Complete me") .priority(Priority::Medium) .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create"); assert!(task.completed_at.is_none()); // Complete the task let completed = repo.complete(task.id, user_id).await.expect("Failed to complete"); assert!(completed.is_some()); let completed = completed.unwrap(); assert_eq!(completed.status, TaskStatus::Completed); assert!(completed.completed_at.is_some(), "completed_at should be set after completing"); } #[tokio::test] async fn test_list_completed_between_filters_by_date() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); // Create and complete two tasks let task1 = NewTask::builder("Task one") .priority(Priority::Medium) .build(); let t1 = repo.create(user_id, task1).await.expect("Failed to create"); repo.complete(t1.id, user_id).await.expect("Failed to complete"); let task2 = NewTask::builder("Task two") .priority(Priority::Low) .build(); let t2 = repo.create(user_id, task2).await.expect("Failed to create"); repo.complete(t2.id, user_id).await.expect("Failed to complete"); // Query with a range that includes now let start = Utc::now() - Duration::minutes(5); let end = Utc::now() + Duration::minutes(5); let results = repo.list_completed_between(user_id, start, end).await.expect("Failed to list"); assert_eq!(results.len(), 2, "Both tasks should be in the date range"); // Query with a range in the past — should return nothing let old_start = Utc::now() - Duration::days(30); let old_end = Utc::now() - Duration::days(29); let results = repo.list_completed_between(user_id, old_start, old_end).await.expect("Failed to list"); assert!(results.is_empty(), "No tasks should match a past date range"); } #[tokio::test] async fn test_update_to_completed_sets_completed_at() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let new_task = NewTask::builder("Update to completed") .priority(Priority::Medium) .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create"); assert!(task.completed_at.is_none()); // Update status to Completed via update() let update = UpdateTask { project_id: None, milestone_id: None, contact_id: None, description: task.description.clone(), status: TaskStatus::Completed, priority: task.priority.clone(), due: task.due, tags: task.tags.clone(), recurrence: Recurrence::None, urgency: task.urgency, scheduled_start: None, scheduled_duration: None, estimated_minutes: None, }; let updated = repo.update(task.id, user_id, update).await.expect("Failed to update"); assert!(updated.is_some()); let updated = updated.unwrap(); assert_eq!(updated.status, TaskStatus::Completed); assert!(updated.completed_at.is_some(), "completed_at should be set when updating to Completed"); } #[tokio::test] async fn test_update_from_completed_clears_completed_at() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let new_task = NewTask::builder("Revert from completed") .priority(Priority::High) .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create"); // Complete it first let completed = repo.complete(task.id, user_id).await.expect("Failed to complete"); assert!(completed.is_some()); assert!(completed.unwrap().completed_at.is_some()); // Now move it back to Pending via update() let update = UpdateTask { project_id: None, milestone_id: None, contact_id: None, description: task.description.clone(), status: TaskStatus::Pending, priority: task.priority.clone(), due: task.due, tags: task.tags.clone(), recurrence: Recurrence::None, urgency: task.urgency, scheduled_start: None, scheduled_duration: None, estimated_minutes: None, }; let reverted = repo.update(task.id, user_id, update).await.expect("Failed to update"); assert!(reverted.is_some()); let reverted = reverted.unwrap(); assert_eq!(reverted.status, TaskStatus::Pending); assert!(reverted.completed_at.is_none(), "completed_at should be cleared when moving away from Completed"); } #[tokio::test] async fn test_snooze_completed_task_fails() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let new_task = NewTask::builder("Completed task") .priority(Priority::Medium) .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create"); // Complete the task repo.complete(task.id, user_id).await.expect("Failed to complete"); // Attempt to snooze the completed task — should fail let until = Utc::now() + Duration::hours(2); let result = repo.snooze(task.id, user_id, until).await; assert!(result.is_err(), "Snoozing a completed task should return an error"); let err = result.unwrap_err(); assert!(err.is_validation(), "Error should be a validation error, got: {}", err); } #[tokio::test] async fn test_snooze_deleted_task_fails() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let new_task = NewTask::builder("Deleted task") .priority(Priority::Medium) .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create"); // Delete the task repo.delete(task.id, user_id).await.expect("Failed to delete"); // Attempt to snooze the deleted task — should fail let until = Utc::now() + Duration::hours(2); let result = repo.snooze(task.id, user_id, until).await; assert!(result.is_err(), "Snoozing a deleted task should return an error"); let err = result.unwrap_err(); assert!(err.is_validation(), "Error should be a validation error, got: {}", err); } #[tokio::test] async fn test_snooze_pending_task_succeeds() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let new_task = NewTask::builder("Pending task") .priority(Priority::Medium) .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create"); // Snooze the pending task — should succeed let until = Utc::now() + Duration::hours(2); let result = repo.snooze(task.id, user_id, until).await; assert!(result.is_ok(), "Snoozing a pending task should succeed"); let snoozed = result.unwrap(); assert!(snoozed.is_some()); assert!(snoozed.unwrap().snoozed_until.is_some()); } #[tokio::test] async fn test_snooze_started_task_succeeds() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let new_task = NewTask::builder("Started task") .priority(Priority::Medium) .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create"); // Start the task first repo.start(task.id, user_id).await.expect("Failed to start"); // Snooze the started task — should succeed let until = Utc::now() + Duration::hours(2); let result = repo.snooze(task.id, user_id, until).await; assert!(result.is_ok(), "Snoozing a started task should succeed"); let snoozed = result.unwrap(); assert!(snoozed.is_some()); assert!(snoozed.unwrap().snoozed_until.is_some()); }