use super::*; use crate::core::db::{create_connection, run_migrations}; use std::path::Path; fn setup_test_db() -> rusqlite::Connection { let conn = create_connection(Path::new(":memory:")).unwrap(); run_migrations(&conn).unwrap(); conn } fn seed_project(conn: &rusqlite::Connection) -> i64 { conn.execute( "INSERT INTO projects (id, gitlab_project_id, path_with_namespace, web_url, created_at, updated_at) VALUES (1, 100, 'group/repo', 'https://gitlab.example.com/group/repo', 1000, 2000)", [], ) .unwrap(); 1 } fn insert_mr( conn: &rusqlite::Connection, id: i64, iid: i64, title: &str, state: &str, merged_at: Option, ) { conn.execute( "INSERT INTO merge_requests (id, gitlab_id, iid, project_id, title, state, author_username, \ created_at, updated_at, last_seen_at, source_branch, target_branch, merged_at, web_url) VALUES (?1, ?2, ?3, 1, ?4, ?5, 'dev', 1000, 2000, 2000, 'feature', 'main', ?6, \ 'https://gitlab.example.com/group/repo/-/merge_requests/' || ?3)", rusqlite::params![id, 300 + id, iid, title, state, merged_at], ) .unwrap(); } fn insert_file_change( conn: &rusqlite::Connection, mr_id: i64, old_path: Option<&str>, new_path: &str, change_type: &str, ) { conn.execute( "INSERT INTO mr_file_changes (merge_request_id, project_id, old_path, new_path, change_type) VALUES (?1, 1, ?2, ?3, ?4)", rusqlite::params![mr_id, old_path, new_path, change_type], ) .unwrap(); } fn insert_entity_ref( conn: &rusqlite::Connection, source_type: &str, source_id: i64, target_type: &str, target_id: i64, ref_type: &str, ) { conn.execute( "INSERT INTO entity_references (project_id, source_entity_type, source_entity_id, \ target_entity_type, target_entity_id, reference_type, source_method, created_at) VALUES (1, ?1, ?2, ?3, ?4, ?5, 'api', 1000)", rusqlite::params![source_type, source_id, target_type, target_id, ref_type], ) .unwrap(); } fn insert_issue(conn: &rusqlite::Connection, id: i64, iid: i64, title: &str, state: &str) { conn.execute( "INSERT INTO issues (id, gitlab_id, project_id, iid, title, state, created_at, updated_at, \ last_seen_at, web_url) VALUES (?1, ?2, 1, ?3, ?4, ?5, 1000, 2000, 2000, \ 'https://gitlab.example.com/group/repo/-/issues/' || ?3)", rusqlite::params![id, 400 + id, iid, title, state], ) .unwrap(); } fn insert_discussion_and_note( conn: &rusqlite::Connection, discussion_id: i64, mr_id: i64, note_id: i64, author: &str, body: &str, position_new_path: Option<&str>, ) { conn.execute( "INSERT INTO discussions (id, gitlab_discussion_id, project_id, merge_request_id, \ noteable_type, last_seen_at) VALUES (?1, 'disc-' || ?1, 1, ?2, 'MergeRequest', 2000)", rusqlite::params![discussion_id, mr_id], ) .unwrap(); conn.execute( "INSERT INTO notes (id, gitlab_id, discussion_id, project_id, author_username, body, \ is_system, created_at, updated_at, last_seen_at, position_new_path) VALUES (?1, ?2, ?3, 1, ?4, ?5, 0, 1500, 1500, 2000, ?6)", rusqlite::params![ note_id, 500 + note_id, discussion_id, author, body, position_new_path ], ) .unwrap(); } #[test] fn test_trace_empty_file() { let conn = setup_test_db(); seed_project(&conn); let result = run_trace(&conn, Some(1), "src/nonexistent.rs", false, false, 10).unwrap(); assert!(result.trace_chains.is_empty()); assert_eq!(result.resolved_paths, ["src/nonexistent.rs"]); } #[test] fn test_trace_finds_mr() { let conn = setup_test_db(); seed_project(&conn); insert_mr(&conn, 1, 10, "Add auth module", "merged", Some(3000)); insert_file_change(&conn, 1, None, "src/auth.rs", "added"); let result = run_trace(&conn, Some(1), "src/auth.rs", false, false, 10).unwrap(); assert_eq!(result.trace_chains.len(), 1); let chain = &result.trace_chains[0]; assert_eq!(chain.mr_iid, 10); assert_eq!(chain.mr_title, "Add auth module"); assert_eq!(chain.mr_state, "merged"); assert_eq!(chain.change_type, "added"); assert!(chain.merged_at_iso.is_some()); } #[test] fn test_trace_follows_renames() { let conn = setup_test_db(); seed_project(&conn); // MR 1: added old_auth.rs insert_mr(&conn, 1, 10, "Add old auth", "merged", Some(1000)); insert_file_change(&conn, 1, None, "src/old_auth.rs", "added"); // MR 2: renamed old_auth.rs -> auth.rs insert_mr(&conn, 2, 11, "Rename auth", "merged", Some(2000)); insert_file_change(&conn, 2, Some("src/old_auth.rs"), "src/auth.rs", "renamed"); // Query auth.rs with follow_renames -- should find both MRs let result = run_trace(&conn, Some(1), "src/auth.rs", true, false, 10).unwrap(); assert!(result.renames_followed); assert!( result .resolved_paths .contains(&"src/old_auth.rs".to_string()) ); assert!(result.resolved_paths.contains(&"src/auth.rs".to_string())); // MR 2 touches auth.rs (new_path), MR 1 touches old_auth.rs (new_path in its row) assert_eq!(result.trace_chains.len(), 2); } #[test] fn test_trace_links_issues() { let conn = setup_test_db(); seed_project(&conn); insert_mr(&conn, 1, 10, "Fix login bug", "merged", Some(3000)); insert_file_change(&conn, 1, None, "src/login.rs", "modified"); insert_issue(&conn, 1, 42, "Login broken on mobile", "closed"); insert_entity_ref(&conn, "merge_request", 1, "issue", 1, "closes"); let result = run_trace(&conn, Some(1), "src/login.rs", false, false, 10).unwrap(); assert_eq!(result.trace_chains.len(), 1); assert_eq!(result.trace_chains[0].issues.len(), 1); let issue = &result.trace_chains[0].issues[0]; assert_eq!(issue.iid, 42); assert_eq!(issue.title, "Login broken on mobile"); assert_eq!(issue.reference_type, "closes"); } #[test] fn test_trace_limits_chains() { let conn = setup_test_db(); seed_project(&conn); for i in 1..=3 { insert_mr( &conn, i, 10 + i, &format!("MR {i}"), "merged", Some(1000 * i), ); insert_file_change(&conn, i, None, "src/shared.rs", "modified"); } let result = run_trace(&conn, Some(1), "src/shared.rs", false, false, 1).unwrap(); assert_eq!(result.trace_chains.len(), 1); } #[test] fn test_trace_no_follow_renames() { let conn = setup_test_db(); seed_project(&conn); // MR 1: added old_name.rs insert_mr(&conn, 1, 10, "Add old file", "merged", Some(1000)); insert_file_change(&conn, 1, None, "src/old_name.rs", "added"); // MR 2: renamed old_name.rs -> new_name.rs insert_mr(&conn, 2, 11, "Rename file", "merged", Some(2000)); insert_file_change( &conn, 2, Some("src/old_name.rs"), "src/new_name.rs", "renamed", ); // Without follow_renames -- should only find MR 2 (new_path = new_name.rs) let result = run_trace(&conn, Some(1), "src/new_name.rs", false, false, 10).unwrap(); assert_eq!(result.resolved_paths, ["src/new_name.rs"]); assert!(!result.renames_followed); assert_eq!(result.trace_chains.len(), 1); assert_eq!(result.trace_chains[0].mr_iid, 11); } #[test] fn test_trace_includes_discussions() { let conn = setup_test_db(); seed_project(&conn); insert_mr(&conn, 1, 10, "Refactor auth", "merged", Some(3000)); insert_file_change(&conn, 1, None, "src/auth.rs", "modified"); insert_discussion_and_note( &conn, 1, 1, 1, "reviewer", "This function should handle the error case.", Some("src/auth.rs"), ); let result = run_trace(&conn, Some(1), "src/auth.rs", false, true, 10).unwrap(); assert_eq!(result.trace_chains.len(), 1); assert_eq!(result.trace_chains[0].discussions.len(), 1); let disc = &result.trace_chains[0].discussions[0]; assert_eq!(disc.author_username, "reviewer"); assert!(disc.body_snippet.contains("error case")); assert_eq!(disc.mr_iid, 10); }