//! Integration tests for SqliteTaskRepository focus + scheduling + reporting methods. mod common; use chrono::{Duration, TimeZone, Utc}; use goingson_core::{NewTask, Priority, TaskCrud, TaskScheduling}; use goingson_db_sqlite::SqliteTaskRepository; // Focus #[tokio::test] async fn test_set_focus_and_list_focused() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let task = repo .create(user_id, NewTask::builder("Focus me").build()) .await .unwrap(); // Not focused initially. assert!(repo.list_focused(user_id).await.unwrap().is_empty()); let focused = repo .set_focus(task.id, user_id, true) .await .expect("set focus") .unwrap(); assert!(focused.is_focus); assert!( focused.focus_set_at.is_some(), "focus_set_at should be stamped" ); let list = repo.list_focused(user_id).await.unwrap(); assert_eq!(list.len(), 1); assert_eq!(list[0].id, task.id); // Clearing focus removes it from the set. let unfocused = repo .set_focus(task.id, user_id, false) .await .expect("clear focus") .unwrap(); assert!(!unfocused.is_focus); assert!( unfocused.focus_set_at.is_none(), "focus_set_at should be nulled" ); assert!(repo.list_focused(user_id).await.unwrap().is_empty()); } #[tokio::test] async fn test_clear_all_focus() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let a = repo .create(user_id, NewTask::builder("A").build()) .await .unwrap(); let b = repo .create(user_id, NewTask::builder("B").build()) .await .unwrap(); repo.set_focus(a.id, user_id, true).await.unwrap(); repo.set_focus(b.id, user_id, true).await.unwrap(); assert_eq!(repo.list_focused(user_id).await.unwrap().len(), 2); let cleared = repo.clear_all_focus(user_id).await.expect("clear all"); assert_eq!(cleared, 2, "both focused tasks should be cleared"); assert!(repo.list_focused(user_id).await.unwrap().is_empty()); // A second call clears nothing. assert_eq!(repo.clear_all_focus(user_id).await.unwrap(), 0); } #[tokio::test] async fn test_focus_is_per_user_scoped() { let pool = common::setup_test_db().await; let user_a = common::create_test_user(&pool).await; let user_b = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let task_a = repo .create(user_a, NewTask::builder("A's task").build()) .await .unwrap(); let task_b = repo .create(user_b, NewTask::builder("B's task").build()) .await .unwrap(); repo.set_focus(task_a.id, user_a, true).await.unwrap(); repo.set_focus(task_b.id, user_b, true).await.unwrap(); // Each user only sees their own focused task. let a_list = repo.list_focused(user_a).await.unwrap(); assert_eq!(a_list.len(), 1); assert_eq!(a_list[0].id, task_a.id); // Clearing all focus for A must not touch B. repo.clear_all_focus(user_a).await.unwrap(); assert!(repo.list_focused(user_a).await.unwrap().is_empty()); let b_list = repo.list_focused(user_b).await.unwrap(); assert_eq!(b_list.len(), 1, "clearing A's focus must not affect B"); assert_eq!(b_list[0].id, task_b.id); } #[tokio::test] async fn test_list_available_for_focus_candidate_set() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); // Candidate: pending, not focused, not snoozed, not waiting. let candidate = repo .create( user_id, NewTask::builder("Candidate") .priority(Priority::High) .build(), ) .await .unwrap(); // Excluded: already focused. let focused = repo .create(user_id, NewTask::builder("Focused").build()) .await .unwrap(); repo.set_focus(focused.id, user_id, true).await.unwrap(); // Excluded: snoozed into the future. let snoozed = repo .create(user_id, NewTask::builder("Snoozed").build()) .await .unwrap(); repo.snooze(snoozed.id, user_id, Utc::now() + Duration::hours(2)) .await .unwrap(); // Excluded: waiting for response. let waiting = repo .create(user_id, NewTask::builder("Waiting").build()) .await .unwrap(); repo.mark_waiting(waiting.id, user_id, None).await.unwrap(); // Excluded: completed. let done = repo .create(user_id, NewTask::builder("Done").build()) .await .unwrap(); repo.complete(done.id, user_id).await.unwrap(); let available = repo .list_available_for_focus(user_id, 100) .await .expect("list available"); let ids: Vec<_> = available.iter().map(|t| t.id).collect(); assert_eq!( ids, vec![candidate.id], "only the pending unfocused task should be a focus candidate" ); } // Scheduling #[tokio::test] async fn test_update_schedule_persists_and_clears() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let task = repo .create(user_id, NewTask::builder("Schedule me").build()) .await .unwrap(); assert!(task.scheduled_start.is_none()); assert!(task.scheduled_duration.is_none()); let start = Utc.with_ymd_and_hms(2026, 8, 15, 14, 0, 0).unwrap(); let updated = repo .update_schedule(task.id, user_id, Some(start), Some(90)) .await .expect("schedule") .unwrap(); assert_eq!(updated.scheduled_start, Some(start)); assert_eq!(updated.scheduled_duration, Some(90)); // Re-fetch to confirm persistence. let fetched = repo.get_by_id(task.id, user_id).await.unwrap().unwrap(); assert_eq!(fetched.scheduled_start, Some(start)); assert_eq!(fetched.scheduled_duration, Some(90)); // Passing None clears the schedule. let cleared = repo .update_schedule(task.id, user_id, None, None) .await .expect("clear schedule") .unwrap(); assert!(cleared.scheduled_start.is_none()); assert!(cleared.scheduled_duration.is_none()); let refetched = repo.get_by_id(task.id, user_id).await.unwrap().unwrap(); assert!(refetched.scheduled_start.is_none()); assert!(refetched.scheduled_duration.is_none()); } #[tokio::test] async fn test_list_scheduled_for_date() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let on_date = repo .create(user_id, NewTask::builder("On date").build()) .await .unwrap(); let off_date = repo .create(user_id, NewTask::builder("Off date").build()) .await .unwrap(); let _unscheduled = repo .create(user_id, NewTask::builder("Unscheduled").build()) .await .unwrap(); let target = Utc.with_ymd_and_hms(2026, 8, 15, 9, 30, 0).unwrap(); let other = Utc.with_ymd_and_hms(2026, 8, 16, 9, 30, 0).unwrap(); repo.update_schedule(on_date.id, user_id, Some(target), Some(60)) .await .unwrap(); repo.update_schedule(off_date.id, user_id, Some(other), Some(60)) .await .unwrap(); let date = chrono::NaiveDate::from_ymd_opt(2026, 8, 15).unwrap(); let scheduled = repo .list_scheduled_for_date(user_id, date) .await .expect("list scheduled"); let ids: Vec<_> = scheduled.iter().map(|t| t.id).collect(); assert_eq!( ids, vec![on_date.id], "only the task scheduled on the target date should match" ); } #[tokio::test] async fn test_list_scheduled_for_date_excludes_completed() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let task = repo .create(user_id, NewTask::builder("Scheduled then done").build()) .await .unwrap(); let start = Utc.with_ymd_and_hms(2026, 8, 15, 9, 30, 0).unwrap(); repo.update_schedule(task.id, user_id, Some(start), Some(60)) .await .unwrap(); let date = chrono::NaiveDate::from_ymd_opt(2026, 8, 15).unwrap(); assert_eq!( repo.list_scheduled_for_date(user_id, date) .await .unwrap() .len(), 1 ); repo.complete(task.id, user_id).await.unwrap(); assert!( repo.list_scheduled_for_date(user_id, date) .await .unwrap() .is_empty(), "completed tasks should not appear in the scheduled-for-date list" ); } // Reporting windows #[tokio::test] async fn test_list_due_between_inclusive_boundaries() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let start = Utc.with_ymd_and_hms(2026, 9, 1, 0, 0, 0).unwrap(); let end = Utc.with_ymd_and_hms(2026, 9, 30, 23, 59, 59).unwrap(); let at_start = repo .create(user_id, NewTask::builder("At start").due(start).build()) .await .unwrap(); let at_end = repo .create(user_id, NewTask::builder("At end").due(end).build()) .await .unwrap(); let inside = repo .create( user_id, NewTask::builder("Inside") .due(Utc.with_ymd_and_hms(2026, 9, 15, 12, 0, 0).unwrap()) .build(), ) .await .unwrap(); let _before = repo .create( user_id, NewTask::builder("Before") .due(start - Duration::seconds(1)) .build(), ) .await .unwrap(); let _after = repo .create( user_id, NewTask::builder("After") .due(end + Duration::seconds(1)) .build(), ) .await .unwrap(); let found = repo .list_due_between(user_id, start, end) .await .expect("list due between"); let ids: Vec<_> = found.iter().map(|t| t.id).collect(); assert_eq!( ids.len(), 3, "window is inclusive on both bounds and excludes tasks just outside" ); for want in [at_start.id, at_end.id, inside.id] { assert!(ids.contains(&want), "expected task {want} in window"); } } #[tokio::test] async fn test_list_due_between_excludes_completed() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let start = Utc.with_ymd_and_hms(2026, 9, 1, 0, 0, 0).unwrap(); let end = Utc.with_ymd_and_hms(2026, 9, 30, 23, 59, 59).unwrap(); let task = repo .create( user_id, NewTask::builder("Due and done") .due(Utc.with_ymd_and_hms(2026, 9, 10, 0, 0, 0).unwrap()) .build(), ) .await .unwrap(); assert_eq!( repo.list_due_between(user_id, start, end) .await .unwrap() .len(), 1 ); repo.complete(task.id, user_id).await.unwrap(); assert!( repo.list_due_between(user_id, start, end) .await .unwrap() .is_empty() ); } #[tokio::test] async fn test_list_became_overdue_between_window() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let start = Utc.with_ymd_and_hms(2026, 9, 1, 0, 0, 0).unwrap(); let end = Utc.with_ymd_and_hms(2026, 9, 30, 23, 59, 59).unwrap(); let inside = repo .create( user_id, NewTask::builder("Overdue inside") .due(Utc.with_ymd_and_hms(2026, 9, 15, 8, 0, 0).unwrap()) .build(), ) .await .unwrap(); let _outside = repo .create( user_id, NewTask::builder("Overdue outside") .due(end + Duration::seconds(1)) .build(), ) .await .unwrap(); // Completed tasks are excluded even if due in range. let done = repo .create( user_id, NewTask::builder("Overdue done") .due(Utc.with_ymd_and_hms(2026, 9, 20, 8, 0, 0).unwrap()) .build(), ) .await .unwrap(); repo.complete(done.id, user_id).await.unwrap(); let found = repo .list_became_overdue_between(user_id, start, end) .await .expect("list overdue between"); let ids: Vec<_> = found.iter().map(|t| t.id).collect(); assert_eq!( ids, vec![inside.id], "only the non-completed task due inside the window should match" ); } #[tokio::test] async fn test_list_created_between_window() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); let a = repo .create(user_id, NewTask::builder("Created one").build()) .await .unwrap(); let b = repo .create(user_id, NewTask::builder("Created two").build()) .await .unwrap(); // A window around now includes both freshly created tasks. let start = Utc::now() - Duration::minutes(5); let end = Utc::now() + Duration::minutes(5); let found = repo .list_created_between(user_id, start, end) .await .expect("list created between"); let ids: Vec<_> = found.iter().map(|t| t.id).collect(); assert!( ids.contains(&a.id) && ids.contains(&b.id), "both tasks created inside the window should match" ); // A past window matches nothing. let old_start = Utc::now() - Duration::days(30); let old_end = Utc::now() - Duration::days(29); assert!( repo.list_created_between(user_id, old_start, old_end) .await .unwrap() .is_empty() ); }