feat: mark pull requests approved by the authenticated user
This commit is contained in:
@@ -7,6 +7,200 @@ fn parse(url: &str, provider: Option<&str>) -> Result<Forge, String> {
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn github_personal_approvals_use_the_viewer_and_last_decisive_review() {
|
||||
let forge = parse("https://github.com/owner/repo", None).unwrap();
|
||||
let values: Vec<_> = (1..=8).map(|id| json!({"number": id})).collect();
|
||||
let metadata = forge.metadata_with(&values, &mut |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
assert_eq!(url, "https://api.github.com/user");
|
||||
return Ok(json!({"login": "viewer"}));
|
||||
}
|
||||
let body: Value = serde_json::from_str(body.unwrap()).unwrap();
|
||||
let query = body["query"].as_str().unwrap();
|
||||
assert!(query.contains("author:\"viewer\""));
|
||||
assert!(query.contains("states:[APPROVED,CHANGES_REQUESTED,DISMISSED],last:1"));
|
||||
assert!(!query.contains("mutation"));
|
||||
Ok(json!({"data":{"repository":{
|
||||
"p1":{"additions":3,"deletions":4,"reviews":{"nodes":[{"state":"APPROVED","author":{"login":"VIEWER"}}]}},
|
||||
"p2":{"reviews":{"nodes":[{"state":"CHANGES_REQUESTED","author":{"login":"viewer"}}]}},
|
||||
"p3":{"reviews":{"nodes":[{"state":"DISMISSED","author":{"login":"viewer"}}]}},
|
||||
"p4":{"reviews":null},
|
||||
"p5":{"reviews":{"nodes":[{"state":"COMMENTED","author":{"login":"viewer"}}]}},
|
||||
"p6":{"reviews":{"nodes":[{"state":"APPROVED","author":{"login":"someone-else"}}]}},
|
||||
"p7":{"reviews":{"nodes":[{"state":"PENDING","author":{"login":"viewer"}}]}},
|
||||
"p8":{"reviews":{"nodes":[{"state":"APPROVED","author":{"login":"viewer"}}]}}
|
||||
}}}))
|
||||
});
|
||||
assert_eq!(
|
||||
metadata
|
||||
.iter()
|
||||
.filter_map(|(id, details)| details.approved.then_some(*id))
|
||||
.collect::<Vec<_>>(),
|
||||
[1, 8]
|
||||
);
|
||||
assert_eq!(metadata[&1].size, Some((3, 4)));
|
||||
assert_eq!(metadata[&8].size, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitlab_personal_approvals_filter_by_viewer_and_follow_connection_pages() {
|
||||
let forge = parse("https://gitlab.com/group/repo", None).unwrap();
|
||||
let values = [json!({"iid":7}), json!({"iid":8}), json!({"iid":9})];
|
||||
let metadata = forge.metadata_with(&values, &mut |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
assert_eq!(url, "https://gitlab.com/api/v4/user");
|
||||
return Ok(json!({"username":"viewer"}));
|
||||
}
|
||||
let body: Value = serde_json::from_str(body.unwrap()).unwrap();
|
||||
let query = body["query"].as_str().unwrap();
|
||||
assert!(query.contains("approvedBy:[\"viewer\"]"));
|
||||
assert!(query.contains("iids:[\"7\",\"8\",\"9\"]"));
|
||||
assert!(!query.contains("mutation"));
|
||||
if query.contains("after:\"next-page\"") {
|
||||
return Ok(json!({"data":{"project":{"approved":{
|
||||
"nodes":[{"iid":"8"}],
|
||||
"pageInfo":{"hasNextPage":false,"endCursor":null}
|
||||
}}}}));
|
||||
}
|
||||
Ok(json!({"data":{"project":{
|
||||
"p7":{"diffStatsSummary":{"additions":5,"deletions":6}},
|
||||
"p8":{"diffStatsSummary":null},
|
||||
"p9":{"diffStatsSummary":{"additions":0,"deletions":0}},
|
||||
"approved":{
|
||||
"nodes":[{"iid":"7"},{"iid":"999"}],
|
||||
"pageInfo":{"hasNextPage":true,"endCursor":"next-page"}
|
||||
}
|
||||
}}}))
|
||||
});
|
||||
assert!(metadata[&7].approved && metadata[&8].approved);
|
||||
assert!(!metadata[&9].approved);
|
||||
assert!(!metadata.contains_key(&999));
|
||||
assert_eq!(metadata[&7].size, Some((5, 6)));
|
||||
assert_eq!(metadata[&8].size, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gitea_personal_approvals_ignore_other_users_and_honor_later_decisions_and_failures() {
|
||||
let forge = parse("https://codeberg.org/owner/repo", None).unwrap();
|
||||
let own = |id, state: &str, dismissed| json!({"id":id,"user":{"id":42},"state":state,"dismissed":dismissed});
|
||||
let approved = own(10, "APPROVED", false);
|
||||
for (pages, expected) in [
|
||||
(
|
||||
vec![
|
||||
Ok(json!([approved.clone()])),
|
||||
Ok(json!([
|
||||
own(30, "COMMENT", false),
|
||||
own(31, "PENDING", false),
|
||||
own(32, "REQUEST_REVIEW", false)
|
||||
])),
|
||||
Ok(json!([])),
|
||||
],
|
||||
true,
|
||||
),
|
||||
(
|
||||
vec![
|
||||
Ok(json!([approved.clone()])),
|
||||
Ok(json!([own(20, "REQUEST_CHANGES", false)])),
|
||||
Ok(json!([])),
|
||||
],
|
||||
false,
|
||||
),
|
||||
(
|
||||
vec![
|
||||
Ok(json!([approved.clone()])),
|
||||
Ok(json!([own(20, "APPROVED", true)])),
|
||||
Ok(json!([])),
|
||||
],
|
||||
false,
|
||||
),
|
||||
(
|
||||
vec![
|
||||
Ok(json!([{"id":10,"user":{"id":7},"state":"APPROVED","dismissed":false}])),
|
||||
Ok(json!([])),
|
||||
],
|
||||
false,
|
||||
),
|
||||
(
|
||||
vec![
|
||||
Ok(json!([approved.clone()])),
|
||||
Err("later review page failed".into()),
|
||||
],
|
||||
false,
|
||||
),
|
||||
(
|
||||
vec![Ok(json!([{"id":10,"user":{"id":42},"state":"APPROVED"}]))],
|
||||
false,
|
||||
),
|
||||
// Select by review identity, not response order; a later approval replaces a rejection.
|
||||
(
|
||||
vec![
|
||||
Ok(json!([
|
||||
own(20, "APPROVED", false),
|
||||
own(10, "REQUEST_CHANGES", false)
|
||||
])),
|
||||
Ok(json!([])),
|
||||
],
|
||||
true,
|
||||
),
|
||||
] {
|
||||
let mut pages = pages.into_iter();
|
||||
let metadata = forge.metadata_with(
|
||||
&[json!({"number":7,"additions":1,"deletions":2})],
|
||||
&mut |url, body| {
|
||||
assert!(body.is_none());
|
||||
if url.ends_with("/user") {
|
||||
assert_eq!(url, "https://codeberg.org/api/v1/user");
|
||||
return Ok(json!({"id":42}));
|
||||
}
|
||||
assert!(url.starts_with(
|
||||
"https://codeberg.org/api/v1/repos/owner/repo/pulls/7/reviews?limit=50&page="
|
||||
));
|
||||
pages.next().expect("unexpected review page")
|
||||
},
|
||||
);
|
||||
assert_eq!(metadata[&7].approved, expected);
|
||||
assert_eq!(metadata[&7].size, Some((1, 2)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unavailable_viewer_or_metadata_never_fabricates_personal_approval() {
|
||||
for (url, provider) in [
|
||||
("https://github.com/owner/repo", Provider::Github),
|
||||
("https://gitlab.com/group/repo", Provider::Gitlab),
|
||||
("https://codeberg.org/owner/repo", Provider::Gitea),
|
||||
] {
|
||||
let forge = parse(url, None).unwrap();
|
||||
for unavailable_viewer in [true, false] {
|
||||
let metadata = forge.metadata_with(
|
||||
&[json!({"number":7,"iid":7,"additions":0,"deletions":0})],
|
||||
&mut |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
return if unavailable_viewer {
|
||||
Err("no API credentials".into())
|
||||
} else {
|
||||
Ok(json!({"login":"viewer","username":"viewer","id":42}))
|
||||
};
|
||||
}
|
||||
if provider == Provider::Gitea {
|
||||
assert!(
|
||||
!unavailable_viewer,
|
||||
"anonymous listings must not fetch reviews"
|
||||
);
|
||||
assert!(body.is_none());
|
||||
}
|
||||
Err("metadata denied".into())
|
||||
},
|
||||
);
|
||||
assert!(metadata.values().all(|details| !details.approved));
|
||||
if provider == Provider::Gitea {
|
||||
assert_eq!(metadata[&7].size, Some((0, 0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draft_summaries_keep_open_closed_and_merged_states_distinct() {
|
||||
for provider in [Provider::Github, Provider::Gitlab, Provider::Gitea] {
|
||||
@@ -62,6 +256,9 @@ fn pull_request_sizes_include_zero_and_preserve_unknown_statistics() {
|
||||
.unwrap();
|
||||
let page = forge
|
||||
.page_with(false, 1, |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
return Err("unauthenticated fixture".into());
|
||||
}
|
||||
if let Some(body) = body {
|
||||
let query: Value = serde_json::from_str(body).unwrap();
|
||||
let query = query["query"].as_str().unwrap();
|
||||
@@ -140,6 +337,9 @@ fn pages_are_bounded_newest_first_summaries_without_historical_metadata() {
|
||||
] {
|
||||
let forge = parse(url, None).unwrap();
|
||||
let page = forge.page_with(false, 3, |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
return Err("unauthenticated fixture".into());
|
||||
}
|
||||
if body.is_some() {
|
||||
return Err("statistics unavailable".into());
|
||||
}
|
||||
@@ -179,6 +379,9 @@ fn gitlab_closed_combines_closed_and_merged_pages_without_open_requests() {
|
||||
let mut states = Vec::new();
|
||||
let result = forge
|
||||
.page_with(true, page, |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
return Err("unauthenticated fixture".into());
|
||||
}
|
||||
if body.is_some() {
|
||||
return Err("statistics unavailable".into());
|
||||
}
|
||||
@@ -215,6 +418,9 @@ fn gitlab_closed_combines_closed_and_merged_pages_without_open_requests() {
|
||||
all.sort_unstable_by(|a, b| b.created_at.cmp(&a.created_at));
|
||||
assert_eq!(all.iter().map(|p| p.number).collect::<Vec<_>>(), [3, 2, 1]);
|
||||
let raced = forge.page_with(true, 1, |url, body| {
|
||||
if url.ends_with("/user") {
|
||||
return Err("unauthenticated fixture".into());
|
||||
}
|
||||
if body.is_some() {
|
||||
return Err("statistics unavailable".into());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user