Files
gitlore/tests/diffnote_position_tests.rs
Taylor Eernisse d338d68191 test: Add comprehensive test suite for MR ingestion
Introduces thorough test coverage for merge request functionality,
following the established testing patterns from issue ingestion.

New test files:
- mr_transformer_tests.rs: NormalizedMergeRequest transformation tests
  covering full MR with all fields, minimal MR, draft detection via
  title prefix and work_in_progress field, label/assignee/reviewer
  extraction, and timestamp conversion

- mr_discussion_tests.rs: MR discussion normalization tests including
  polymorphic noteable binding, DiffNote position extraction with
  line ranges and SHA triplet, and resolvable note handling

- diffnote_position_tests.rs: Exhaustive DiffNote position scenarios
  covering text/image/file types, single-line vs multi-line comments,
  added/removed/modified lines, and missing position handling

New fixtures:
- fixtures/gitlab_merge_request.json: Representative MR API response
  with nested structures for integration testing

Updated tests:
- gitlab_types_tests.rs: Add MR type deserialization tests
- migration_tests.rs: Update expected schema version to 6

Test design follows property-based patterns where feasible, with
explicit edge case coverage for nullable fields and API variants
across different GitLab versions.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 22:47:17 -05:00

382 lines
12 KiB
Rust

//! Tests for DiffNote position extraction in note transformer.
use gi::gitlab::transformers::discussion::transform_notes_with_diff_position;
use gi::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<GitLabNote>) -> 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);
}