//! Integration tests for time tracking (time_sessions) on SqliteTaskRepository. mod common; use goingson_core::TaskRepository; use goingson_db_sqlite::SqliteTaskRepository; #[tokio::test] async fn test_start_timer_creates_active_session() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let task_id = common::create_test_task(&pool, user_id).await; let repo = SqliteTaskRepository::new(pool); let session = repo .start_timer(task_id, user_id) .await .expect("Failed to start timer"); assert_eq!(session.task_id, task_id); assert!(session.is_active()); assert!(session.ended_at.is_none()); assert!(session.duration_minutes.is_none()); } #[tokio::test] async fn test_start_timer_fails_if_already_active() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let task_id = common::create_test_task(&pool, user_id).await; let repo = SqliteTaskRepository::new(pool); repo.start_timer(task_id, user_id) .await .expect("First start should succeed"); let result = repo.start_timer(task_id, user_id).await; assert!(result.is_err(), "Second start should fail"); let err = result.unwrap_err().to_string(); assert!( err.contains("already running"), "Error should mention already running: {err}" ); } #[tokio::test] async fn test_stop_timer_sets_ended_at_and_duration() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let task_id = common::create_test_task(&pool, user_id).await; let repo = SqliteTaskRepository::new(pool); repo.start_timer(task_id, user_id) .await .expect("Failed to start timer"); let stopped = repo .stop_timer(task_id, user_id) .await .expect("Failed to stop timer"); assert!(stopped.is_some(), "Should return the stopped session"); let session = stopped.unwrap(); assert!(!session.is_active()); assert!(session.ended_at.is_some()); assert!(session.duration_minutes.is_some()); } #[tokio::test] async fn test_stop_timer_updates_actual_minutes_cache() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let task_id = common::create_test_task(&pool, user_id).await; let repo = SqliteTaskRepository::new(pool); repo.start_timer(task_id, user_id) .await .expect("Failed to start timer"); repo.stop_timer(task_id, user_id) .await .expect("Failed to stop timer"); // Fetch the task and check actual_minutes is updated let task = repo .get_by_id(task_id, user_id) .await .expect("Failed to get task"); assert!(task.is_some(), "Task should exist"); // actual_minutes should be >= 0 (the session was nearly instant) assert!(task.unwrap().actual_minutes >= 0); } #[tokio::test] async fn test_discard_timer_removes_session() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let task_id = common::create_test_task(&pool, user_id).await; let repo = SqliteTaskRepository::new(pool); repo.start_timer(task_id, user_id) .await .expect("Failed to start timer"); let discarded = repo .discard_timer(task_id, user_id) .await .expect("Failed to discard timer"); assert!(discarded, "Should return true when session was discarded"); // No active timer should remain let active = repo .get_active_timer(user_id) .await .expect("Failed to get active timer"); assert!(active.is_none(), "No active timer after discard"); // actual_minutes should NOT have been updated let task = repo .get_by_id(task_id, user_id) .await .expect("Failed to get task") .unwrap(); assert_eq!(task.actual_minutes, 0, "Discard should not update actual_minutes"); } #[tokio::test] async fn test_get_active_timer_returns_task_description() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let task_id = common::create_test_task(&pool, user_id).await; let repo = SqliteTaskRepository::new(pool); repo.start_timer(task_id, user_id) .await .expect("Failed to start timer"); let active = repo .get_active_timer(user_id) .await .expect("Failed to get active timer"); assert!(active.is_some(), "Should have an active timer"); let (session, description) = active.unwrap(); assert_eq!(session.task_id, task_id); assert!(!description.is_empty(), "Should include task description"); } #[tokio::test] async fn test_list_time_sessions_for_task() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let task_id = common::create_test_task(&pool, user_id).await; let repo = SqliteTaskRepository::new(pool); // Session 1: start and stop repo.start_timer(task_id, user_id).await.unwrap(); repo.stop_timer(task_id, user_id).await.unwrap(); // Session 2: start and stop repo.start_timer(task_id, user_id).await.unwrap(); repo.stop_timer(task_id, user_id).await.unwrap(); let sessions = repo .list_time_sessions(task_id, user_id) .await .expect("Failed to list sessions"); assert_eq!(sessions.len(), 2, "Should have 2 completed sessions"); assert!( sessions.iter().all(|s| !s.is_active()), "All sessions should be stopped" ); } #[tokio::test] async fn test_multiple_sessions_accumulate_actual_minutes() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let task_id = common::create_test_task(&pool, user_id).await; let repo = SqliteTaskRepository::new(pool); // Session 1 repo.start_timer(task_id, user_id).await.unwrap(); repo.stop_timer(task_id, user_id).await.unwrap(); // Session 2 repo.start_timer(task_id, user_id).await.unwrap(); repo.stop_timer(task_id, user_id).await.unwrap(); let task = repo.get_by_id(task_id, user_id).await.unwrap().unwrap(); // Both sessions were near-instant so actual_minutes >= 0 // The important thing is it didn't error and the cache works assert!(task.actual_minutes >= 0); } #[tokio::test] async fn test_get_time_summary() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let task_id = common::create_test_task(&pool, user_id).await; let repo = SqliteTaskRepository::new(pool); // Create a completed session repo.start_timer(task_id, user_id).await.unwrap(); repo.stop_timer(task_id, user_id).await.unwrap(); let now = chrono::Utc::now(); let week_ago = now - chrono::Duration::days(7); let week_ahead = now + chrono::Duration::days(7); let summaries = repo .get_time_summary(user_id, week_ago, week_ahead) .await .expect("Failed to get time summary"); assert!(!summaries.is_empty(), "Should have at least one summary entry"); assert!(summaries[0].session_count > 0, "Should have session count"); } #[tokio::test] async fn test_estimated_minutes_persists_on_create() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteTaskRepository::new(pool); // Create task with estimated_minutes let new_task = goingson_core::NewTaskBuilder::new("Estimated task".to_string()) .estimated_minutes(45) .build(); let task = repo.create(user_id, new_task).await.expect("Failed to create task"); assert_eq!(task.estimated_minutes, Some(45)); assert_eq!(task.actual_minutes, 0); // Fetch it back to verify persistence let fetched = repo.get_by_id(task.id, user_id).await.unwrap().unwrap(); assert_eq!(fetched.estimated_minutes, Some(45)); } #[tokio::test] async fn test_task_with_active_session_has_active_session_populated() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let task_id = common::create_test_task(&pool, user_id).await; let repo = SqliteTaskRepository::new(pool); // Start a timer repo.start_timer(task_id, user_id).await.unwrap(); // Fetch the task — active_session should be populated let task = repo.get_by_id(task_id, user_id).await.unwrap().unwrap(); assert!(task.active_session.is_some(), "Task with running timer should have active_session"); assert!(task.has_active_timer()); }