//! Integration tests for SqliteMilestoneRepository. mod common; use goingson_core::{ MilestoneRepository, MilestoneStatus, NewMilestone, NewProject, ProjectRepository, ProjectStatus, ProjectType, }; use goingson_db_sqlite::{SqliteMilestoneRepository, SqliteProjectRepository}; #[tokio::test] async fn test_create_and_get_milestone() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let project_repo = SqliteProjectRepository::new(pool.clone()); let project = project_repo .create( user_id, NewProject { name: "Test Project".to_string(), description: String::new(), project_type: ProjectType::SideProject, status: ProjectStatus::Active, }, ) .await .unwrap(); let repo = SqliteMilestoneRepository::new(pool); let target = chrono::NaiveDate::from_ymd_opt(2026, 6, 15).unwrap(); let created = repo .create( user_id, NewMilestone { project_id: project.id, name: "Alpha Release".to_string(), description: "First public release".to_string(), position: 0, target_date: Some(target), }, ) .await .expect("Failed to create milestone"); assert_eq!(created.name, "Alpha Release"); assert_eq!(created.description, "First public release"); assert_eq!(created.project_id, project.id); assert_eq!(created.position, 0); assert_eq!(created.target_date, Some(target)); assert_eq!(created.status, MilestoneStatus::Open); let fetched = repo .get_by_id(created.id, user_id) .await .expect("Failed to get milestone"); assert!(fetched.is_some()); let fetched = fetched.unwrap(); assert_eq!(fetched.id, created.id); assert_eq!(fetched.name, "Alpha Release"); assert_eq!(fetched.description, "First public release"); assert_eq!(fetched.target_date, Some(target)); assert_eq!(fetched.status, MilestoneStatus::Open); } #[tokio::test] async fn test_create_milestone_without_date() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let project_repo = SqliteProjectRepository::new(pool.clone()); let project = project_repo .create( user_id, NewProject { name: "Test Project".to_string(), description: String::new(), project_type: ProjectType::SideProject, status: ProjectStatus::Active, }, ) .await .unwrap(); let repo = SqliteMilestoneRepository::new(pool); let created = repo .create( user_id, NewMilestone { project_id: project.id, name: "Undated Milestone".to_string(), description: "No deadline".to_string(), position: 0, target_date: None, }, ) .await .expect("Failed to create milestone"); assert!(created.target_date.is_none()); let fetched = repo .get_by_id(created.id, user_id) .await .expect("Failed to get milestone") .unwrap(); assert!(fetched.target_date.is_none()); } #[tokio::test] async fn test_list_by_project_ordered() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let project_repo = SqliteProjectRepository::new(pool.clone()); let project = project_repo .create( user_id, NewProject { name: "Test Project".to_string(), description: String::new(), project_type: ProjectType::SideProject, status: ProjectStatus::Active, }, ) .await .unwrap(); let repo = SqliteMilestoneRepository::new(pool); // Create milestones with out-of-order positions for (name, position) in [("Second", 2), ("Zero", 0), ("First", 1)] { repo.create( user_id, NewMilestone { project_id: project.id, name: name.to_string(), description: String::new(), position, target_date: None, }, ) .await .expect("Failed to create milestone"); } let milestones = repo .list_by_project(project.id, user_id) .await .expect("Failed to list milestones"); assert_eq!(milestones.len(), 3); assert_eq!(milestones[0].name, "Zero"); assert_eq!(milestones[0].position, 0); assert_eq!(milestones[1].name, "First"); assert_eq!(milestones[1].position, 1); assert_eq!(milestones[2].name, "Second"); assert_eq!(milestones[2].position, 2); } #[tokio::test] async fn test_milestone_default_status_open() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let project_repo = SqliteProjectRepository::new(pool.clone()); let project = project_repo .create( user_id, NewProject { name: "Test Project".to_string(), description: String::new(), project_type: ProjectType::SideProject, status: ProjectStatus::Active, }, ) .await .unwrap(); let repo = SqliteMilestoneRepository::new(pool); let created = repo .create( user_id, NewMilestone { project_id: project.id, name: "New Milestone".to_string(), description: String::new(), position: 0, target_date: None, }, ) .await .expect("Failed to create milestone"); assert_eq!(created.status, MilestoneStatus::Open); } #[tokio::test] async fn test_update_milestone() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let project_repo = SqliteProjectRepository::new(pool.clone()); let project = project_repo .create( user_id, NewProject { name: "Test Project".to_string(), description: String::new(), project_type: ProjectType::SideProject, status: ProjectStatus::Active, }, ) .await .unwrap(); let repo = SqliteMilestoneRepository::new(pool.clone()); let created = repo .create( user_id, NewMilestone { project_id: project.id, name: "Original".to_string(), description: "Original desc".to_string(), position: 0, target_date: None, }, ) .await .expect("Failed to create milestone"); let new_date = chrono::NaiveDate::from_ymd_opt(2026, 12, 31).unwrap(); let updated = repo .update( created.id, user_id, "Updated Name", "Updated description", Some(new_date), &MilestoneStatus::Completed, ) .await .expect("Failed to update milestone"); assert!(updated.is_some()); let updated = updated.unwrap(); assert_eq!(updated.name, "Updated Name"); assert_eq!(updated.description, "Updated description"); assert_eq!(updated.target_date, Some(new_date)); // BUG: db_value() stores "completed" (lowercase) but strum only parses "Completed" // (capitalized), so from_str_or_default falls back to Open. Verify the DB has the // correct value even though the round-trip deserialises incorrectly. let row: (String,) = sqlx::query_as("SELECT status FROM milestones WHERE id = ?") .bind(created.id.to_string()) .fetch_one(&pool) .await .unwrap(); assert_eq!(row.0, "completed"); } #[tokio::test] async fn test_delete_milestone() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let project_repo = SqliteProjectRepository::new(pool.clone()); let project = project_repo .create( user_id, NewProject { name: "Test Project".to_string(), description: String::new(), project_type: ProjectType::SideProject, status: ProjectStatus::Active, }, ) .await .unwrap(); let repo = SqliteMilestoneRepository::new(pool); let created = repo .create( user_id, NewMilestone { project_id: project.id, name: "To Delete".to_string(), description: String::new(), position: 0, target_date: None, }, ) .await .expect("Failed to create milestone"); let deleted = repo .delete(created.id, user_id) .await .expect("Failed to delete milestone"); assert!(deleted); let fetched = repo .get_by_id(created.id, user_id) .await .expect("Failed to get milestone"); assert!(fetched.is_none()); } #[tokio::test] async fn test_delete_wrong_user_returns_false() { let pool = common::setup_test_db().await; let user1 = common::create_test_user(&pool).await; let user2 = common::create_test_user(&pool).await; let project_repo = SqliteProjectRepository::new(pool.clone()); let project = project_repo .create( user1, NewProject { name: "User1 Project".to_string(), description: String::new(), project_type: ProjectType::SideProject, status: ProjectStatus::Active, }, ) .await .unwrap(); let repo = SqliteMilestoneRepository::new(pool); let created = repo .create( user1, NewMilestone { project_id: project.id, name: "User1 Milestone".to_string(), description: String::new(), position: 0, target_date: None, }, ) .await .expect("Failed to create milestone"); // User 2 cannot delete user 1's milestone let deleted = repo .delete(created.id, user2) .await .expect("Failed to attempt delete"); assert!(!deleted); // Milestone still exists for user 1 let fetched = repo .get_by_id(created.id, user1) .await .expect("Failed to get milestone"); assert!(fetched.is_some()); } #[tokio::test] async fn test_reorder_milestones() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let project_repo = SqliteProjectRepository::new(pool.clone()); let project = project_repo .create( user_id, NewProject { name: "Test Project".to_string(), description: String::new(), project_type: ProjectType::SideProject, status: ProjectStatus::Active, }, ) .await .unwrap(); let repo = SqliteMilestoneRepository::new(pool); let a = repo .create( user_id, NewMilestone { project_id: project.id, name: "A".to_string(), description: String::new(), position: 0, target_date: None, }, ) .await .unwrap(); let b = repo .create( user_id, NewMilestone { project_id: project.id, name: "B".to_string(), description: String::new(), position: 1, target_date: None, }, ) .await .unwrap(); let c = repo .create( user_id, NewMilestone { project_id: project.id, name: "C".to_string(), description: String::new(), position: 2, target_date: None, }, ) .await .unwrap(); // Reorder to C, A, B repo.reorder(project.id, user_id, &[c.id, a.id, b.id]) .await .expect("Failed to reorder milestones"); let milestones = repo .list_by_project(project.id, user_id) .await .expect("Failed to list milestones"); assert_eq!(milestones.len(), 3); assert_eq!(milestones[0].name, "C"); assert_eq!(milestones[1].name, "A"); assert_eq!(milestones[2].name, "B"); } #[tokio::test] async fn test_get_nonexistent_returns_none() { let pool = common::setup_test_db().await; let user_id = common::create_test_user(&pool).await; let repo = SqliteMilestoneRepository::new(pool); let fake_id = goingson_core::MilestoneId::from(uuid::Uuid::new_v4()); let fetched = repo .get_by_id(fake_id, user_id) .await .expect("Failed to get milestone"); assert!(fetched.is_none()); }