//! 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()); }