//! Tests for DiffNote position extraction in note transformer. use lore::gitlab::transformers::discussion::transform_notes_with_diff_position; use lore::gitlab::types::{ GitLabAuthor, GitLabDiscussion, GitLabLineRange, GitLabLineRangePoint, GitLabNote, GitLabNotePosition, }; fn make_author() -> GitLabAuthor { GitLabAuthor { id: 1, username: "testuser".to_string(), name: "Test User".to_string(), } } fn make_basic_note(id: i64, created_at: &str) -> GitLabNote { GitLabNote { id, note_type: Some("DiscussionNote".to_string()), body: format!("Note {}", id), author: make_author(), created_at: created_at.to_string(), updated_at: created_at.to_string(), system: false, resolvable: false, resolved: false, resolved_by: None, resolved_at: None, position: None, } } fn make_diffnote_with_position( id: i64, created_at: &str, position: GitLabNotePosition, ) -> GitLabNote { GitLabNote { id, note_type: Some("DiffNote".to_string()), body: format!("DiffNote {}", id), author: make_author(), created_at: created_at.to_string(), updated_at: created_at.to_string(), system: false, resolvable: true, resolved: false, resolved_by: None, resolved_at: None, position: Some(position), } } fn make_discussion(notes: Vec) -> GitLabDiscussion { GitLabDiscussion { id: "abc123".to_string(), individual_note: false, notes, } } // === DiffNote Position Field Extraction === #[test] fn extracts_position_paths_from_diffnote() { let position = GitLabNotePosition { old_path: Some("src/old.rs".to_string()), new_path: Some("src/new.rs".to_string()), old_line: Some(10), new_line: Some(15), position_type: Some("text".to_string()), line_range: None, base_sha: None, start_sha: None, head_sha: None, }; let note = make_diffnote_with_position(1, "2024-01-16T09:00:00.000Z", position); let discussion = make_discussion(vec![note]); let notes = transform_notes_with_diff_position(&discussion, 100).unwrap(); assert_eq!(notes.len(), 1); assert_eq!(notes[0].position_old_path, Some("src/old.rs".to_string())); assert_eq!(notes[0].position_new_path, Some("src/new.rs".to_string())); assert_eq!(notes[0].position_old_line, Some(10)); assert_eq!(notes[0].position_new_line, Some(15)); } #[test] fn extracts_position_type_from_diffnote() { let position = GitLabNotePosition { old_path: None, new_path: Some("image.png".to_string()), old_line: None, new_line: None, position_type: Some("image".to_string()), line_range: None, base_sha: None, start_sha: None, head_sha: None, }; let note = make_diffnote_with_position(1, "2024-01-16T09:00:00.000Z", position); let discussion = make_discussion(vec![note]); let notes = transform_notes_with_diff_position(&discussion, 100).unwrap(); assert_eq!(notes[0].position_type, Some("image".to_string())); } #[test] fn extracts_sha_triplet_from_diffnote() { let position = GitLabNotePosition { old_path: Some("file.rs".to_string()), new_path: Some("file.rs".to_string()), old_line: Some(5), new_line: Some(5), position_type: Some("text".to_string()), line_range: None, base_sha: Some("abc123base".to_string()), start_sha: Some("def456start".to_string()), head_sha: Some("ghi789head".to_string()), }; let note = make_diffnote_with_position(1, "2024-01-16T09:00:00.000Z", position); let discussion = make_discussion(vec![note]); let notes = transform_notes_with_diff_position(&discussion, 100).unwrap(); assert_eq!(notes[0].position_base_sha, Some("abc123base".to_string())); assert_eq!(notes[0].position_start_sha, Some("def456start".to_string())); assert_eq!(notes[0].position_head_sha, Some("ghi789head".to_string())); } #[test] fn extracts_line_range_from_multiline_diffnote() { let line_range = GitLabLineRange { start: GitLabLineRangePoint { line_code: Some("abc123_10_10".to_string()), line_type: Some("new".to_string()), old_line: None, new_line: Some(10), }, end: GitLabLineRangePoint { line_code: Some("abc123_15_15".to_string()), line_type: Some("new".to_string()), old_line: None, new_line: Some(15), }, }; let position = GitLabNotePosition { old_path: None, new_path: Some("file.rs".to_string()), old_line: None, new_line: Some(10), position_type: Some("text".to_string()), line_range: Some(line_range), base_sha: None, start_sha: None, head_sha: None, }; let note = make_diffnote_with_position(1, "2024-01-16T09:00:00.000Z", position); let discussion = make_discussion(vec![note]); let notes = transform_notes_with_diff_position(&discussion, 100).unwrap(); assert_eq!(notes[0].position_line_range_start, Some(10)); assert_eq!(notes[0].position_line_range_end, Some(15)); } #[test] fn line_range_uses_old_line_fallback_when_new_line_missing() { let line_range = GitLabLineRange { start: GitLabLineRangePoint { line_code: None, line_type: Some("old".to_string()), old_line: Some(20), new_line: None, // missing - should fall back to old_line }, end: GitLabLineRangePoint { line_code: None, line_type: Some("old".to_string()), old_line: Some(25), new_line: None, }, }; let position = GitLabNotePosition { old_path: Some("deleted.rs".to_string()), new_path: None, old_line: Some(20), new_line: None, position_type: Some("text".to_string()), line_range: Some(line_range), base_sha: None, start_sha: None, head_sha: None, }; let note = make_diffnote_with_position(1, "2024-01-16T09:00:00.000Z", position); let discussion = make_discussion(vec![note]); let notes = transform_notes_with_diff_position(&discussion, 100).unwrap(); assert_eq!(notes[0].position_line_range_start, Some(20)); assert_eq!(notes[0].position_line_range_end, Some(25)); } // === Regular Notes (non-DiffNote) === #[test] fn regular_note_has_none_for_all_position_fields() { let note = make_basic_note(1, "2024-01-16T09:00:00.000Z"); let discussion = make_discussion(vec![note]); let notes = transform_notes_with_diff_position(&discussion, 100).unwrap(); assert_eq!(notes[0].position_old_path, None); assert_eq!(notes[0].position_new_path, None); assert_eq!(notes[0].position_old_line, None); assert_eq!(notes[0].position_new_line, None); assert_eq!(notes[0].position_type, None); assert_eq!(notes[0].position_line_range_start, None); assert_eq!(notes[0].position_line_range_end, None); assert_eq!(notes[0].position_base_sha, None); assert_eq!(notes[0].position_start_sha, None); assert_eq!(notes[0].position_head_sha, None); } // === Strict Timestamp Parsing === #[test] fn returns_error_for_invalid_created_at_timestamp() { let mut note = make_basic_note(1, "2024-01-16T09:00:00.000Z"); note.created_at = "not-a-timestamp".to_string(); let discussion = make_discussion(vec![note]); let result = transform_notes_with_diff_position(&discussion, 100); assert!(result.is_err()); let err = result.unwrap_err(); assert!(err.contains("not-a-timestamp")); } #[test] fn returns_error_for_invalid_updated_at_timestamp() { let mut note = make_basic_note(1, "2024-01-16T09:00:00.000Z"); note.updated_at = "garbage".to_string(); let discussion = make_discussion(vec![note]); let result = transform_notes_with_diff_position(&discussion, 100); assert!(result.is_err()); } #[test] fn returns_error_for_invalid_resolved_at_timestamp() { let mut note = make_basic_note(1, "2024-01-16T09:00:00.000Z"); note.resolvable = true; note.resolved = true; note.resolved_by = Some(make_author()); note.resolved_at = Some("bad-timestamp".to_string()); let discussion = make_discussion(vec![note]); let result = transform_notes_with_diff_position(&discussion, 100); assert!(result.is_err()); } // === Mixed Discussion (DiffNote + Regular Notes) === #[test] fn handles_mixed_diffnote_and_regular_notes() { let position = GitLabNotePosition { old_path: None, new_path: Some("file.rs".to_string()), old_line: None, new_line: Some(42), position_type: Some("text".to_string()), line_range: None, base_sha: None, start_sha: None, head_sha: None, }; let diffnote = make_diffnote_with_position(1, "2024-01-16T09:00:00.000Z", position); let regular_note = make_basic_note(2, "2024-01-16T10:00:00.000Z"); let discussion = make_discussion(vec![diffnote, regular_note]); let notes = transform_notes_with_diff_position(&discussion, 100).unwrap(); assert_eq!(notes.len(), 2); // First note is DiffNote with position assert_eq!(notes[0].position_new_path, Some("file.rs".to_string())); assert_eq!(notes[0].position_new_line, Some(42)); // Second note is regular with None position fields assert_eq!(notes[1].position_new_path, None); assert_eq!(notes[1].position_new_line, None); } // === Position Preservation === #[test] fn preserves_note_position_index() { let pos1 = GitLabNotePosition { old_path: None, new_path: Some("file.rs".to_string()), old_line: None, new_line: Some(10), position_type: Some("text".to_string()), line_range: None, base_sha: None, start_sha: None, head_sha: None, }; let pos2 = GitLabNotePosition { old_path: None, new_path: Some("file.rs".to_string()), old_line: None, new_line: Some(20), position_type: Some("text".to_string()), line_range: None, base_sha: None, start_sha: None, head_sha: None, }; let note1 = make_diffnote_with_position(1, "2024-01-16T09:00:00.000Z", pos1); let note2 = make_diffnote_with_position(2, "2024-01-16T10:00:00.000Z", pos2); let discussion = make_discussion(vec![note1, note2]); let notes = transform_notes_with_diff_position(&discussion, 100).unwrap(); assert_eq!(notes[0].position, 0); assert_eq!(notes[1].position, 1); } // === Edge Cases === #[test] fn handles_diffnote_with_empty_position_fields() { // DiffNote exists but all position fields are None let position = GitLabNotePosition { old_path: None, new_path: None, old_line: None, new_line: None, position_type: None, line_range: None, base_sha: None, start_sha: None, head_sha: None, }; let note = make_diffnote_with_position(1, "2024-01-16T09:00:00.000Z", position); let discussion = make_discussion(vec![note]); let notes = transform_notes_with_diff_position(&discussion, 100).unwrap(); // All position fields should be None, not cause an error assert_eq!(notes[0].position_old_path, None); assert_eq!(notes[0].position_new_path, None); } #[test] fn handles_file_position_type() { let position = GitLabNotePosition { old_path: None, new_path: Some("binary.bin".to_string()), old_line: None, new_line: None, position_type: Some("file".to_string()), line_range: None, base_sha: None, start_sha: None, head_sha: None, }; let note = make_diffnote_with_position(1, "2024-01-16T09:00:00.000Z", position); let discussion = make_discussion(vec![note]); let notes = transform_notes_with_diff_position(&discussion, 100).unwrap(); assert_eq!(notes[0].position_type, Some("file".to_string())); assert_eq!(notes[0].position_new_path, Some("binary.bin".to_string())); // File-level comments have no line numbers assert_eq!(notes[0].position_new_line, None); }