use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use travelagent_core::forge::*;
use travelagent_core::model::FileStatus;
use travelagent_forge_github::GitHubForge;
fn load_fixture(name: &str) -> String {
std::fs::read_to_string(format!("tests/fixtures/{name}")).unwrap()
}
fn ripgrep_pr_id() -> PrId {
PrId {
owner: "BurntSushi".into(),
repo: "ripgrep".into(),
number: 2900,
}
}
fn ruff_pr_id() -> PrId {
PrId {
owner: "astral-sh".into(),
repo: "ruff".into(),
number: 16000,
}
}
async fn setup() -> (MockServer, GitHubForge) {
let server = MockServer::start().await;
let forge = GitHubForge::with_token_insecure(&server.uri(), "test-token".into()).unwrap();
(server, forge)
}
#[tokio::test]
async fn ripgrep_pr_metadata_parsed_correctly() {
let (server, forge) = setup().await;
let body: serde_json::Value =
serde_json::from_str(&load_fixture("ripgrep_2900_pr.json")).unwrap();
Mock::given(method("GET"))
.and(path("/repos/BurntSushi/ripgrep/pulls/2900"))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.mount(&server)
.await;
let pr = forge.get_pr(&ripgrep_pr_id()).await.unwrap();
assert_eq!(pr.title, "globset: add matches_all method");
assert_eq!(pr.author, "tmccombs");
assert_eq!(pr.state, PrState::Closed);
assert_eq!(pr.base_branch, "master");
assert_eq!(pr.head_branch, "matches-all");
assert!(!pr.is_draft);
}
#[tokio::test]
async fn ripgrep_commits_parsed_correctly() {
let (server, forge) = setup().await;
let body: serde_json::Value =
serde_json::from_str(&load_fixture("ripgrep_2900_commits.json")).unwrap();
Mock::given(method("GET"))
.and(path("/repos/BurntSushi/ripgrep/pulls/2900/commits"))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.mount(&server)
.await;
let commits = forge.get_pr_commits(&ripgrep_pr_id()).await.unwrap();
assert_eq!(commits.len(), 2);
assert_eq!(commits[0].id, "3c17c22ef64e78064d8c621b118d7cdb3652fa76");
assert_eq!(commits[0].short_id, "3c17c22");
assert_eq!(commits[0].summary, "globset: add matches_all method");
assert_eq!(commits[0].author, "tmccombs");
assert_eq!(commits[1].summary, "fixup! globset: add matches_all method");
}
#[tokio::test]
async fn ripgrep_files_with_patches_parsed_correctly() {
let (server, forge) = setup().await;
let body: serde_json::Value =
serde_json::from_str(&load_fixture("ripgrep_2900_files.json")).unwrap();
Mock::given(method("GET"))
.and(path("/repos/BurntSushi/ripgrep/pulls/2900/files"))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.mount(&server)
.await;
let files = forge.get_pr_files(&ripgrep_pr_id()).await.unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].status, FileStatus::Modified);
assert_eq!(
files[0].new_path,
Some(std::path::PathBuf::from("crates/globset/src/lib.rs"))
);
assert!(
!files[0].hunks.is_empty(),
"Expected hunks to be parsed from the patch"
);
let first_hunk = &files[0].hunks[0];
assert_eq!(first_hunk.old_start, 351);
assert_eq!(first_hunk.new_start, 351);
}
#[tokio::test]
async fn ripgrep_review_comments_parsed_correctly() {
let (server, forge) = setup().await;
let comments_body: serde_json::Value =
serde_json::from_str(&load_fixture("ripgrep_2900_comments.json")).unwrap();
Mock::given(method("GET"))
.and(path("/repos/BurntSushi/ripgrep/pulls/2900/comments"))
.respond_with(ResponseTemplate::new(200).set_body_json(&comments_body))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/repos/BurntSushi/ripgrep/issues/2900/comments"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
.mount(&server)
.await;
let comments = forge.get_comments(&ripgrep_pr_id()).await.unwrap();
assert_eq!(comments.len(), 3);
assert_eq!(comments[0].author, "BurntSushi");
assert_eq!(comments[0].path, Some("crates/globset/src/lib.rs".into()));
assert_eq!(comments[0].in_reply_to, None);
assert_eq!(comments[1].author, "BurntSushi");
assert_eq!(comments[1].in_reply_to, None);
assert_eq!(comments[2].author, "BurntSushi");
assert_eq!(comments[2].in_reply_to, Some(1766859217));
}
#[tokio::test]
async fn ruff_medium_pr_13_files_parsed_correctly() {
let (server, forge) = setup().await;
let body: serde_json::Value =
serde_json::from_str(&load_fixture("ruff_16000_files.json")).unwrap();
Mock::given(method("GET"))
.and(path("/repos/astral-sh/ruff/pulls/16000/files"))
.respond_with(ResponseTemplate::new(200).set_body_json(&body))
.mount(&server)
.await;
let files = forge.get_pr_files(&ruff_pr_id()).await.unwrap();
assert_eq!(files.len(), 13);
let added_count = files
.iter()
.filter(|f| f.status == FileStatus::Added)
.count();
let modified_count = files
.iter()
.filter(|f| f.status == FileStatus::Modified)
.count();
assert_eq!(added_count, 1);
assert_eq!(modified_count, 12);
let added = files
.iter()
.find(|f| f.status == FileStatus::Added)
.unwrap();
assert_eq!(
added.new_path,
Some(std::path::PathBuf::from(
"crates/red_knot_project/src/metadata/settings.rs"
))
);
assert!(added.old_path.is_none());
for file in &files {
assert!(
!file.hunks.is_empty(),
"File {:?} should have hunks parsed from patch",
file.new_path
);
}
}
#[tokio::test]
async fn ruff_3_review_comments_with_replies() {
let (server, forge) = setup().await;
let comments_body: serde_json::Value =
serde_json::from_str(&load_fixture("ruff_16000_comments.json")).unwrap();
Mock::given(method("GET"))
.and(path("/repos/astral-sh/ruff/pulls/16000/comments"))
.respond_with(ResponseTemplate::new(200).set_body_json(&comments_body))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/repos/astral-sh/ruff/issues/16000/comments"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([])))
.mount(&server)
.await;
let comments = forge.get_comments(&ruff_pr_id()).await.unwrap();
assert_eq!(comments.len(), 3);
assert_eq!(comments[0].author, "dhruvmanila");
assert_eq!(comments[0].id, 1946208119);
assert_eq!(comments[0].in_reply_to, None);
assert_eq!(
comments[0].path,
Some("crates/red_knot_project/src/metadata/settings.rs".into())
);
assert_eq!(comments[1].author, "MichaReiser");
assert_eq!(comments[1].in_reply_to, Some(1946208119));
assert_eq!(comments[2].author, "dhruvmanila");
assert_eq!(comments[2].in_reply_to, Some(1946208119));
}