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:
374
tests/mr_transformer_tests.rs
Normal file
374
tests/mr_transformer_tests.rs
Normal 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());
|
||||
}
|
||||
Reference in New Issue
Block a user