//! Integration tests for SqliteStatsRepository. mod common; use chrono::{Duration, Utc}; use goingson_core::{ EmailRepository, EventRepository, NewEmail, NewEvent, NewProject, NewTask, Priority, ProjectRepository, ProjectStatus, ProjectType, Recurrence, StatsRepository, TaskCrud, }; use goingson_db_sqlite::{ SqliteEmailRepository, SqliteEventRepository, SqliteProjectRepository, SqliteStatsRepository, SqliteTaskRepository, }; #[tokio::test] async fn test_dashboard_stats_empty_db_is_all_zeros() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteStatsRepository::new(pool); let now = Utc::now(); let stats = repo .get_dashboard_stats( user_id, now, now - Duration::hours(1), now + Duration::hours(12), now + Duration::days(7), ) .await .expect("get_dashboard_stats"); assert_eq!(stats.tasks_due_today, 0); assert_eq!(stats.tasks_due_this_week, 0); assert_eq!(stats.overdue_count, 0); assert_eq!(stats.unread_emails, 0); assert_eq!(stats.upcoming_events, 0); assert_eq!(stats.active_projects, 0); assert!(stats.high_urgency_tasks.is_empty()); } #[tokio::test] async fn test_dashboard_stats_populated_counts_match() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let tasks = SqliteTaskRepository::new(pool.clone()); let events = SqliteEventRepository::new(pool.clone()); let emails = SqliteEmailRepository::new(pool.clone()); let projects = SqliteProjectRepository::new(pool.clone()); let stats_repo = SqliteStatsRepository::new(pool); let now = Utc::now(); let today_start = now - Duration::hours(1); let tomorrow_start = now + Duration::hours(12); let week_end = now + Duration::days(7); // Task due later today (inside [today_start, tomorrow_start)). tasks .create( user_id, NewTask::builder("Due today") .priority(Priority::High) .due(now + Duration::hours(2)) .build(), ) .await .expect("create today task"); // Task due in three days: this week but not today. tasks .create( user_id, NewTask::builder("Due this week") .priority(Priority::Medium) .due(now + Duration::days(3)) .build(), ) .await .expect("create week task"); // Overdue task. tasks .create( user_id, NewTask::builder("Overdue") .priority(Priority::Low) .due(now - Duration::days(2)) .build(), ) .await .expect("create overdue task"); // Completed task is excluded from every task count and from high-urgency. let done = tasks .create( user_id, NewTask::builder("Already done") .priority(Priority::High) .due(now + Duration::hours(3)) .build(), ) .await .expect("create done task"); tasks.complete(done.id, user_id).await.expect("complete"); // One unread email (counted) and one read email (not counted). emails .create( user_id, NewEmail { project_id: None, from_address: "a@example.com".into(), to_address: "me@example.com".into(), subject: "Unread".into(), body: "body".into(), is_read: false, received_at: Some(now), }, ) .await .expect("create unread email"); let read = emails .create( user_id, NewEmail { project_id: None, from_address: "b@example.com".into(), to_address: "me@example.com".into(), subject: "Read".into(), body: "body".into(), is_read: true, received_at: Some(now), }, ) .await .expect("create read email"); assert!(read.is_read); // One upcoming event (within 7 days) and one far-future event (excluded). events .create( user_id, NewEvent { user_id: Some(user_id), project_id: None, contact_id: None, title: "Upcoming".into(), description: String::new(), start_time: now + Duration::days(1), end_time: None, location: None, linked_task_id: None, recurrence: Recurrence::None, recurrence_rule: None, block_type: None, reminder_offsets_seconds: Vec::new(), }, ) .await .expect("create upcoming event"); events .create( user_id, NewEvent { user_id: Some(user_id), project_id: None, contact_id: None, title: "Far off".into(), description: String::new(), start_time: now + Duration::days(30), end_time: None, location: None, linked_task_id: None, recurrence: Recurrence::None, recurrence_rule: None, block_type: None, reminder_offsets_seconds: Vec::new(), }, ) .await .expect("create far event"); // One active project (counted) and one on-hold project (excluded). projects .create( user_id, NewProject { name: "Active work".into(), description: String::new(), project_type: ProjectType::default(), status: ProjectStatus::Active, }, ) .await .expect("create active project"); projects .create( user_id, NewProject { name: "Paused work".into(), description: String::new(), project_type: ProjectType::default(), status: ProjectStatus::OnHold, }, ) .await .expect("create on-hold project"); let stats = stats_repo .get_dashboard_stats(user_id, now, today_start, tomorrow_start, week_end) .await .expect("get_dashboard_stats"); assert_eq!( stats.tasks_due_today, 1, "only the task due later today counts" ); assert_eq!( stats.tasks_due_this_week, 2, "today's task plus the three-day task" ); assert_eq!(stats.overdue_count, 1, "only the past-due task counts"); assert_eq!(stats.unread_emails, 1, "only the unread email counts"); assert_eq!( stats.upcoming_events, 1, "only the within-a-week event counts" ); assert_eq!(stats.active_projects, 1, "only the Active project counts"); // Three non-completed tasks remain; the completed one is excluded. assert_eq!( stats.high_urgency_tasks.len(), 3, "completed task excluded from high-urgency list" ); assert!( stats .high_urgency_tasks .iter() .all(|t| t.status != "Completed") ); // The list is ordered by urgency descending. for pair in stats.high_urgency_tasks.windows(2) { assert!( pair[0].urgency >= pair[1].urgency, "high-urgency tasks must be sorted descending" ); } } #[tokio::test] async fn test_dashboard_stats_scoped_to_user() { let pool = common::setup_test_db().await; let mine = common::create_test_user(&pool).await; let other = common::create_test_user(&pool).await; let tasks = SqliteTaskRepository::new(pool.clone()); let stats_repo = SqliteStatsRepository::new(pool); let now = Utc::now(); // Another user's overdue task must not leak into my dashboard. tasks .create( other, NewTask::builder("Not mine") .priority(Priority::High) .due(now - Duration::days(1)) .build(), ) .await .expect("create other task"); let stats = stats_repo .get_dashboard_stats( mine, now, now - Duration::hours(1), now + Duration::hours(12), now + Duration::days(7), ) .await .expect("get_dashboard_stats"); assert_eq!( stats.overdue_count, 0, "stats must be scoped to the requesting user" ); assert!(stats.high_urgency_tasks.is_empty()); }