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>
This commit is contained in:
Taylor Eernisse
2026-01-26 22:47:17 -05:00
parent 8ddc974b89
commit d338d68191
6 changed files with 1130 additions and 3 deletions

View File

@@ -0,0 +1,374 @@
//! Tests for MR transformer module.
use gi::gitlab::transformers::merge_request::transform_merge_request;
use gi::gitlab::types::{GitLabAuthor, GitLabMergeRequest, GitLabReferences, GitLabReviewer};
fn make_test_mr() -> GitLabMergeRequest {
GitLabMergeRequest {
id: 12345,
iid: 42,
project_id: 100,
title: "Add user authentication".to_string(),
description: Some("Implements JWT auth flow".to_string()),
state: "merged".to_string(),
draft: false,
work_in_progress: false,
source_branch: "feature/auth".to_string(),
target_branch: "main".to_string(),
sha: Some("abc123def456".to_string()),
references: Some(GitLabReferences {
short: "!42".to_string(),
full: "group/project!42".to_string(),
}),
detailed_merge_status: Some("mergeable".to_string()),
merge_status_legacy: Some("can_be_merged".to_string()),
created_at: "2024-01-15T10:00:00.000Z".to_string(),
updated_at: "2024-01-20T14:30:00.000Z".to_string(),
merged_at: Some("2024-01-20T14:30:00.000Z".to_string()),
closed_at: None,
author: GitLabAuthor {
id: 1,
username: "johndoe".to_string(),
name: "John Doe".to_string(),
},
merge_user: Some(GitLabAuthor {
id: 2,
username: "janedoe".to_string(),
name: "Jane Doe".to_string(),
}),
merged_by: Some(GitLabAuthor {
id: 2,
username: "janedoe".to_string(),
name: "Jane Doe".to_string(),
}),
labels: vec!["enhancement".to_string(), "auth".to_string()],
assignees: vec![GitLabAuthor {
id: 3,
username: "bob".to_string(),
name: "Bob Smith".to_string(),
}],
reviewers: vec![GitLabReviewer {
id: 4,
username: "alice".to_string(),
name: "Alice Wong".to_string(),
}],
web_url: "https://gitlab.example.com/group/project/-/merge_requests/42".to_string(),
}
}
#[test]
fn transforms_mr_with_all_fields() {
let mr = make_test_mr();
let result = transform_merge_request(&mr, 200).unwrap();
assert_eq!(result.merge_request.gitlab_id, 12345);
assert_eq!(result.merge_request.iid, 42);
assert_eq!(result.merge_request.project_id, 200); // Local project ID, not GitLab's
assert_eq!(result.merge_request.title, "Add user authentication");
assert_eq!(
result.merge_request.description,
Some("Implements JWT auth flow".to_string())
);
assert_eq!(result.merge_request.state, "merged");
assert!(!result.merge_request.draft);
assert_eq!(result.merge_request.author_username, "johndoe");
assert_eq!(result.merge_request.source_branch, "feature/auth");
assert_eq!(result.merge_request.target_branch, "main");
assert_eq!(
result.merge_request.head_sha,
Some("abc123def456".to_string())
);
assert_eq!(
result.merge_request.references_short,
Some("!42".to_string())
);
assert_eq!(
result.merge_request.references_full,
Some("group/project!42".to_string())
);
assert_eq!(
result.merge_request.detailed_merge_status,
Some("mergeable".to_string())
);
assert_eq!(
result.merge_request.merge_user_username,
Some("janedoe".to_string())
);
assert_eq!(
result.merge_request.web_url,
"https://gitlab.example.com/group/project/-/merge_requests/42"
);
}
#[test]
fn parses_timestamps_to_ms_epoch() {
let mr = make_test_mr();
let result = transform_merge_request(&mr, 200).unwrap();
// 2024-01-15T10:00:00.000Z = 1705312800000 ms
assert_eq!(result.merge_request.created_at, 1705312800000);
// 2024-01-20T14:30:00.000Z = 1705761000000 ms
assert_eq!(result.merge_request.updated_at, 1705761000000);
// merged_at should also be parsed
assert_eq!(result.merge_request.merged_at, Some(1705761000000));
}
#[test]
fn handles_timezone_offset_timestamps() {
let mut mr = make_test_mr();
// GitLab can return timestamps with timezone offset
mr.created_at = "2024-01-15T05:00:00-05:00".to_string();
let result = transform_merge_request(&mr, 200).unwrap();
// 05:00 EST = 10:00 UTC = same as original test
assert_eq!(result.merge_request.created_at, 1705312800000);
}
#[test]
fn sets_last_seen_at_to_current_time() {
let mr = make_test_mr();
let before = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
let result = transform_merge_request(&mr, 200).unwrap();
let after = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as i64;
assert!(result.merge_request.last_seen_at >= before);
assert!(result.merge_request.last_seen_at <= after);
}
#[test]
fn extracts_label_names() {
let mr = make_test_mr();
let result = transform_merge_request(&mr, 200).unwrap();
assert_eq!(result.label_names.len(), 2);
assert_eq!(result.label_names[0], "enhancement");
assert_eq!(result.label_names[1], "auth");
}
#[test]
fn handles_empty_labels() {
let mut mr = make_test_mr();
mr.labels = vec![];
let result = transform_merge_request(&mr, 200).unwrap();
assert!(result.label_names.is_empty());
}
#[test]
fn extracts_assignee_usernames() {
let mr = make_test_mr();
let result = transform_merge_request(&mr, 200).unwrap();
assert_eq!(result.assignee_usernames.len(), 1);
assert_eq!(result.assignee_usernames[0], "bob");
}
#[test]
fn extracts_reviewer_usernames() {
let mr = make_test_mr();
let result = transform_merge_request(&mr, 200).unwrap();
assert_eq!(result.reviewer_usernames.len(), 1);
assert_eq!(result.reviewer_usernames[0], "alice");
}
#[test]
fn handles_empty_assignees_and_reviewers() {
let mut mr = make_test_mr();
mr.assignees = vec![];
mr.reviewers = vec![];
let result = transform_merge_request(&mr, 200).unwrap();
assert!(result.assignee_usernames.is_empty());
assert!(result.reviewer_usernames.is_empty());
}
#[test]
fn draft_prefers_draft_field() {
let mut mr = make_test_mr();
mr.draft = true;
mr.work_in_progress = false;
let result = transform_merge_request(&mr, 200).unwrap();
assert!(result.merge_request.draft);
}
#[test]
fn draft_falls_back_to_work_in_progress() {
let mut mr = make_test_mr();
mr.draft = false;
mr.work_in_progress = true;
let result = transform_merge_request(&mr, 200).unwrap();
assert!(result.merge_request.draft);
}
#[test]
fn draft_false_when_both_false() {
let mut mr = make_test_mr();
mr.draft = false;
mr.work_in_progress = false;
let result = transform_merge_request(&mr, 200).unwrap();
assert!(!result.merge_request.draft);
}
#[test]
fn detailed_merge_status_prefers_non_legacy() {
let mut mr = make_test_mr();
mr.detailed_merge_status = Some("checking".to_string());
mr.merge_status_legacy = Some("can_be_merged".to_string());
let result = transform_merge_request(&mr, 200).unwrap();
assert_eq!(
result.merge_request.detailed_merge_status,
Some("checking".to_string())
);
}
#[test]
fn detailed_merge_status_falls_back_to_legacy() {
let mut mr = make_test_mr();
mr.detailed_merge_status = None;
mr.merge_status_legacy = Some("can_be_merged".to_string());
let result = transform_merge_request(&mr, 200).unwrap();
assert_eq!(
result.merge_request.detailed_merge_status,
Some("can_be_merged".to_string())
);
}
#[test]
fn merge_user_prefers_merge_user_field() {
let mut mr = make_test_mr();
mr.merge_user = Some(GitLabAuthor {
id: 10,
username: "merge_user_name".to_string(),
name: "Merge User".to_string(),
});
mr.merged_by = Some(GitLabAuthor {
id: 11,
username: "merged_by_name".to_string(),
name: "Merged By".to_string(),
});
let result = transform_merge_request(&mr, 200).unwrap();
assert_eq!(
result.merge_request.merge_user_username,
Some("merge_user_name".to_string())
);
}
#[test]
fn merge_user_falls_back_to_merged_by() {
let mut mr = make_test_mr();
mr.merge_user = None;
mr.merged_by = Some(GitLabAuthor {
id: 11,
username: "merged_by_name".to_string(),
name: "Merged By".to_string(),
});
let result = transform_merge_request(&mr, 200).unwrap();
assert_eq!(
result.merge_request.merge_user_username,
Some("merged_by_name".to_string())
);
}
#[test]
fn handles_missing_references() {
let mut mr = make_test_mr();
mr.references = None;
let result = transform_merge_request(&mr, 200).unwrap();
assert!(result.merge_request.references_short.is_none());
assert!(result.merge_request.references_full.is_none());
}
#[test]
fn handles_missing_sha() {
let mut mr = make_test_mr();
mr.sha = None;
let result = transform_merge_request(&mr, 200).unwrap();
assert!(result.merge_request.head_sha.is_none());
}
#[test]
fn handles_missing_description() {
let mut mr = make_test_mr();
mr.description = None;
let result = transform_merge_request(&mr, 200).unwrap();
assert!(result.merge_request.description.is_none());
}
#[test]
fn handles_closed_at_timestamp() {
let mut mr = make_test_mr();
mr.state = "closed".to_string();
mr.merged_at = None;
mr.closed_at = Some("2024-01-18T12:00:00.000Z".to_string());
let result = transform_merge_request(&mr, 200).unwrap();
assert!(result.merge_request.merged_at.is_none());
// 2024-01-18T12:00:00.000Z = 1705579200000 ms
assert_eq!(result.merge_request.closed_at, Some(1705579200000));
}
#[test]
fn passes_through_locked_state() {
let mut mr = make_test_mr();
mr.state = "locked".to_string();
let result = transform_merge_request(&mr, 200).unwrap();
assert_eq!(result.merge_request.state, "locked");
}
#[test]
fn returns_error_for_invalid_created_at() {
let mut mr = make_test_mr();
mr.created_at = "not-a-timestamp".to_string();
let result = transform_merge_request(&mr, 200);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("not-a-timestamp"));
}
#[test]
fn returns_error_for_invalid_updated_at() {
let mut mr = make_test_mr();
mr.updated_at = "invalid".to_string();
let result = transform_merge_request(&mr, 200);
assert!(result.is_err());
}
#[test]
fn returns_error_for_invalid_merged_at() {
let mut mr = make_test_mr();
mr.merged_at = Some("bad-timestamp".to_string());
let result = transform_merge_request(&mr, 200);
assert!(result.is_err());
}
#[test]
fn returns_error_for_invalid_closed_at() {
let mut mr = make_test_mr();
mr.closed_at = Some("garbage".to_string());
let result = transform_merge_request(&mr, 200);
assert!(result.is_err());
}