use gitee_cli_rs::api::client::Client;
use gitee_cli_rs::api::pulls::{CreatePr, EditPr, PrCommentPositional, PrFilter};
use gitee_cli_rs::api::StateChange;
use gitee_cli_rs::models::{MergeMethod, PrState};
use gitee_cli_rs::repo::Repo;
const PULL_REQUEST_JSON: &str = include_str!("fixtures/pull_request.json");
const PR_FILE_DIFF_JSON: &str = include_str!("fixtures/pr_file_diff.json");
fn client(server: &mockito::ServerGuard) -> Client {
Client::new(format!("{}/api/v5", server.url()), "fake-token".into())
}
fn api_path(path: &str) -> String {
format!("/api/v5{path}")
}
fn test_repo() -> Repo {
Repo {
owner: "oschina".to_string(),
name: "gitee-cli".to_string(),
}
}
#[test]
fn list_hits_pulls_path_with_state_and_author_single_page() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls";
let mock = server
.mock("GET", api_path(path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page".into(), "1".into()),
mockito::Matcher::UrlEncoded("per_page".into(), "100".into()),
mockito::Matcher::UrlEncoded("state".into(), "open".into()),
mockito::Matcher::UrlEncoded("author".into(), "dev1".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(format!("[{PULL_REQUEST_JSON}]"))
.create();
let items = client(&server)
.pulls(&test_repo())
.list(&PrFilter {
state: Some("open"),
author: Some("dev1"),
limit: 50,
..Default::default()
})
.expect("list should succeed");
mock.assert();
assert_eq!(items.len(), 1);
assert_eq!(items[0].number, 12);
assert_eq!(items[0].title, "Add pagination helpers");
}
#[test]
fn get_deserializes_pull_request() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12";
let mock = server
.mock("GET", api_path(path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let pr = client(&server)
.pulls(&test_repo())
.get(12)
.expect("get should succeed");
mock.assert();
assert_eq!(pr.number, 12);
assert_eq!(pr.head.git_ref, "feature/paging");
assert_eq!(pr.base.git_ref, "master");
}
const PR_COMMITS_JSON: &str = r#"[
{
"sha": "abc1234567890deadbeef00000000000000000000",
"html_url": "https://gitee.com/oschina/gitee-cli/commit/abc1234567890",
"commit": {
"message": "Add pagination helpers\n\nBody paragraph.",
"author": {
"name": "Dev One",
"email": "dev1@example.com",
"date": "2026-01-01T10:00:00+08:00"
}
},
"author": {"login": "dev1"}
},
{
"sha": "def4567890abcdef1234567890abcdef12345678",
"html_url": "https://gitee.com/oschina/gitee-cli/commit/def4567890",
"commit": {
"message": "Fix edge case",
"author": {
"name": "Dev Two",
"email": "dev2@example.com",
"date": "2026-01-02T11:00:00+08:00"
}
},
"author": {"login": "dev2"}
},
{
"sha": "fedcba0987654321fedcba0987654321fedcba09",
"html_url": "https://gitee.com/oschina/gitee-cli/commit/fedcba09",
"commit": {
"message": "Third commit",
"author": {
"name": "Dev Three",
"date": "2026-01-03T12:00:00+08:00"
}
}
}
]"#;
#[test]
fn commits_hits_pull_commits_path_and_deserializes_nested_commit() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/commits";
let mock = server
.mock("GET", api_path(path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_COMMITS_JSON)
.create();
let items = client(&server)
.pulls(&test_repo())
.commits(12, 30)
.expect("commits should succeed");
mock.assert();
assert_eq!(items.len(), 3);
assert_eq!(
items[0].sha,
"abc1234567890deadbeef00000000000000000000"
);
assert_eq!(items[0].subject(), "Add pagination helpers");
assert_eq!(items[0].author_label(), "dev1");
assert_eq!(
items[0].date(),
Some("2026-01-01T10:00:00+08:00")
);
assert_eq!(
items[0].html_url.as_deref(),
Some("https://gitee.com/oschina/gitee-cli/commit/abc1234567890")
);
assert_eq!(items[1].subject(), "Fix edge case");
assert_eq!(items[2].author_label(), "Dev Three");
}
#[test]
fn commits_respects_limit_client_side() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/commits";
let mock = server
.mock("GET", api_path(path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_COMMITS_JSON)
.create();
let items = client(&server)
.pulls(&test_repo())
.commits(12, 2)
.expect("commits with limit should succeed");
mock.assert();
assert_eq!(items.len(), 2);
assert_eq!(items[0].subject(), "Add pagination helpers");
assert_eq!(items[1].subject(), "Fix edge case");
}
#[test]
fn files_deserializes_file_diffs() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/files";
let mock = server
.mock("GET", api_path(path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_FILE_DIFF_JSON)
.create();
let files = client(&server)
.pulls(&test_repo())
.files(12)
.expect("files should succeed");
mock.assert();
assert_eq!(files.len(), 2);
assert_eq!(files[0].path, "pom.xml");
assert!(files[0].patch.as_ref().unwrap().contains("3.5.15"));
assert!(files[1].patch.is_none());
}
#[test]
fn create_posts_form_title_head_base_body() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls";
let mock = server
.mock("POST", api_path(path).as_str())
.match_body(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("title".into(), "New feature".into()),
mockito::Matcher::UrlEncoded("head".into(), "feature/x".into()),
mockito::Matcher::UrlEncoded("base".into(), "master".into()),
mockito::Matcher::UrlEncoded("body".into(), "Please review".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let pr = client(&server)
.pulls(&test_repo())
.create(&CreatePr {
title: "New feature",
head: "feature/x",
base: "master",
body: Some("Please review"),
..Default::default()
})
.expect("create should succeed");
mock.assert();
assert_eq!(pr.number, 12);
}
#[test]
fn create_sends_full_parity_fields() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls";
let mock = server
.mock("POST", api_path(path).as_str())
.match_body(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("title".into(), "New feature".into()),
mockito::Matcher::UrlEncoded("labels".into(), "bug,ui".into()),
mockito::Matcher::UrlEncoded("assignees".into(), "me".into()),
mockito::Matcher::UrlEncoded("testers".into(), "qa1".into()),
mockito::Matcher::UrlEncoded("milestone_number".into(), "7".into()),
mockito::Matcher::UrlEncoded("issue".into(), "I1AB2C".into()),
mockito::Matcher::UrlEncoded("close_related_issue".into(), "true".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
client(&server)
.pulls(&test_repo())
.create(&CreatePr {
title: "New feature",
head: "feature/x",
base: "master",
labels: Some("bug,ui"),
assignees: Some("me"),
testers: Some("qa1"),
milestone_number: Some(7),
issue: Some("I1AB2C"),
close_related_issue: true,
..Default::default()
})
.expect("create should succeed");
mock.assert();
}
#[test]
fn file_contents_decodes_base64_file() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/contents/PULL_REQUEST_TEMPLATE.md";
let mock = server
.mock("GET", api_path(path).as_str())
.match_query(mockito::Matcher::UrlEncoded("ref".into(), "master".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"{"encoding":"base64","content":"IyMgVGVtcGxhdGUK"}"#)
.create();
let text = client(&server)
.repos()
.file_contents("oschina", "gitee-cli", "PULL_REQUEST_TEMPLATE.md", "master")
.expect("file_contents should succeed");
mock.assert();
assert_eq!(text.as_deref(), Some("## Template\n"));
}
#[test]
fn file_contents_404_is_none() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/contents/.gitee/PULL_REQUEST_TEMPLATE.md";
let mock = server
.mock("GET", api_path(path).as_str())
.match_query(mockito::Matcher::UrlEncoded("ref".into(), "master".into()))
.with_status(404)
.with_header("content-type", "application/json")
.with_body(r#"{"message":"Not Found"}"#)
.create();
let text = client(&server)
.repos()
.file_contents(
"oschina",
"gitee-cli",
".gitee/PULL_REQUEST_TEMPLATE.md",
"master",
)
.expect("404 should map to Ok(None)");
mock.assert();
assert_eq!(text, None);
}
#[test]
fn merge_puts_squash_and_close_related_issue_true() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/merge";
let mock = server
.mock("PUT", api_path(path).as_str())
.match_body(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("merge_method".into(), "squash".into()),
mockito::Matcher::UrlEncoded("close_related_issue".into(), "true".into()),
]))
.with_status(200)
.create();
client(&server)
.pulls(&test_repo())
.merge(12, MergeMethod::Squash, true)
.expect("merge should succeed");
mock.assert();
}
#[test]
fn merge_puts_merge_and_close_related_issue_false() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/merge";
let mock = server
.mock("PUT", api_path(path).as_str())
.match_body(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("merge_method".into(), "merge".into()),
mockito::Matcher::UrlEncoded("close_related_issue".into(), "false".into()),
]))
.with_status(200)
.create();
client(&server)
.pulls(&test_repo())
.merge(12, MergeMethod::Merge, false)
.expect("merge should succeed");
mock.assert();
}
#[test]
fn create_sends_draft_true_when_set() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls";
let mock = server
.mock("POST", api_path(path).as_str())
.match_body(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("title".into(), "WIP".into()),
mockito::Matcher::UrlEncoded("head".into(), "feature/x".into()),
mockito::Matcher::UrlEncoded("base".into(), "master".into()),
mockito::Matcher::UrlEncoded("draft".into(), "true".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
client(&server)
.pulls(&test_repo())
.create(&CreatePr {
title: "WIP",
head: "feature/x",
base: "master",
draft: true,
..Default::default()
})
.expect("create draft should succeed");
mock.assert();
}
#[test]
fn set_state_patches_form_state_closed() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12";
let mock = server
.mock("PATCH", api_path(path).as_str())
.match_body(mockito::Matcher::UrlEncoded(
"state".into(),
"closed".into(),
))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let pr = client(&server)
.pulls(&test_repo())
.set_state(12, PrState::Closed)
.expect("set_state should succeed");
mock.assert();
assert_eq!(pr.number, 12);
}
#[test]
fn set_state_idempotent_open_pr_patches_and_returns_changed() {
let mut server = mockito::Server::new();
let get_path = "/repos/oschina/gitee-cli/pulls/12";
let patch_path = "/repos/oschina/gitee-cli/pulls/12";
server
.mock("GET", api_path(get_path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let patch = server
.mock("PATCH", api_path(patch_path).as_str())
.match_body(mockito::Matcher::UrlEncoded("state".into(), "closed".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let change = client(&server)
.pulls(&test_repo())
.set_state_idempotent(12, PrState::Closed)
.expect("idempotent close should succeed");
patch.assert();
assert!(matches!(change, StateChange::Changed(_)));
}
#[test]
fn set_state_idempotent_already_closed_skips_patch() {
let mut server = mockito::Server::new();
let get_path = "/repos/oschina/gitee-cli/pulls/12";
let closed_body = r#"{"number":12,"title":"x","state":"closed","html_url":"https://gitee.com/x","head":{"ref":"f"},"base":{"ref":"master"}}"#;
let get = server
.mock("GET", api_path(get_path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(closed_body)
.create();
let patch = server
.mock("PATCH", api_path(get_path).as_str())
.expect(0)
.create();
let change = client(&server)
.pulls(&test_repo())
.set_state_idempotent(12, PrState::Closed)
.expect("idempotent close should succeed");
get.assert();
patch.assert();
assert!(matches!(change, StateChange::Already(_)));
}
#[test]
fn set_draft_patches_form_draft_false() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12";
let mock = server
.mock("PATCH", api_path(path).as_str())
.match_body(mockito::Matcher::UrlEncoded("draft".into(), "false".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let pr = client(&server)
.pulls(&test_repo())
.set_draft(12, false)
.expect("set_draft should succeed");
mock.assert();
assert_eq!(pr.number, 12);
}
#[test]
fn set_draft_idempotent_already_ready_skips_patch() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12";
let get = server
.mock("GET", api_path(path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let patch = server
.mock("PATCH", api_path(path).as_str())
.expect(0)
.create();
let change = client(&server)
.pulls(&test_repo())
.set_draft_idempotent(12, false)
.expect("idempotent ready should succeed");
get.assert();
patch.assert();
assert!(matches!(change, StateChange::Already(_)));
}
#[test]
fn set_draft_idempotent_draft_pr_patches_ready() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12";
let draft_body = r#"{"number":12,"title":"WIP","state":"open","draft":true,"html_url":"https://gitee.com/x","head":{"ref":"f"},"base":{"ref":"master"}}"#;
let get = server
.mock("GET", api_path(path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(draft_body)
.create();
let patch = server
.mock("PATCH", api_path(path).as_str())
.match_body(mockito::Matcher::UrlEncoded("draft".into(), "false".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let change = client(&server)
.pulls(&test_repo())
.set_draft_idempotent(12, false)
.expect("idempotent ready should succeed");
get.assert();
patch.assert();
assert!(matches!(change, StateChange::Changed(_)));
}
#[test]
fn merge_idempotent_already_merged_skips_put() {
let mut server = mockito::Server::new();
let get_path = "/repos/oschina/gitee-cli/pulls/12";
let merged_body = r#"{"number":12,"title":"x","state":"merged","html_url":"https://gitee.com/x","head":{"ref":"f"},"base":{"ref":"master"}}"#;
let get = server
.mock("GET", api_path(get_path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(merged_body)
.create();
let put = server
.mock("PUT", api_path("/repos/oschina/gitee-cli/pulls/12/merge").as_str())
.expect(0)
.create();
let change = client(&server)
.pulls(&test_repo())
.merge_idempotent(12, MergeMethod::Merge, true)
.expect("idempotent merge should succeed");
get.assert();
put.assert();
assert!(matches!(change, StateChange::Already(())));
}
#[test]
fn merge_idempotent_open_pr_puts_and_returns_changed() {
let mut server = mockito::Server::new();
let get_path = "/repos/oschina/gitee-cli/pulls/12";
server
.mock("GET", api_path(get_path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let put = server
.mock("PUT", api_path("/repos/oschina/gitee-cli/pulls/12/merge").as_str())
.with_status(200)
.create();
let change = client(&server)
.pulls(&test_repo())
.merge_idempotent(12, MergeMethod::Squash, false)
.expect("idempotent merge should succeed");
put.assert();
assert!(matches!(change, StateChange::Changed(())));
}
#[test]
fn comment_posts_form_body() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/comments";
let response = r#"{"id":99,"body":"LGTM","html_url":"https://gitee.com/oschina/gitee-cli/pulls/12#note_99"}"#;
let mock = server
.mock("POST", api_path(path).as_str())
.match_body(mockito::Matcher::UrlEncoded("body".into(), "LGTM".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(response)
.create();
let comment = client(&server)
.pulls(&test_repo())
.comment(12, "LGTM", &PrCommentPositional::default())
.expect("comment should succeed");
mock.assert();
assert_eq!(comment.body, "LGTM");
}
#[test]
fn comment_posts_positional_form_fields() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/comments";
let response = r#"{"id":100,"body":"nit","path":"src/main.rs","position":"7","commit_id":"deadbeef","comment_type":"diff_comment"}"#;
let mock = server
.mock("POST", api_path(path).as_str())
.match_body(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("body".into(), "nit".into()),
mockito::Matcher::UrlEncoded("path".into(), "src/main.rs".into()),
mockito::Matcher::UrlEncoded("position".into(), "7".into()),
mockito::Matcher::UrlEncoded("commit_id".into(), "deadbeef".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(response)
.create();
let positional = PrCommentPositional {
path: Some("src/main.rs"),
position: Some(7),
commit_id: Some("deadbeef"),
};
let comment = client(&server)
.pulls(&test_repo())
.comment(12, "nit", &positional)
.expect("positional comment should succeed");
mock.assert();
assert_eq!(comment.body, "nit");
assert_eq!(comment.path.as_deref(), Some("src/main.rs"));
assert_eq!(comment.position.as_deref(), Some("7"));
assert_eq!(comment.commit_id.as_deref(), Some("deadbeef"));
assert_eq!(comment.comment_type.as_deref(), Some("diff_comment"));
}
#[test]
fn list_comments_hits_pull_comments_path_and_decodes_pr_comment() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/comments";
let body = r#"[
{
"id": 99,
"body": "general note",
"user": {"login": "dev1"},
"created_at": "2026-01-01T00:00:00+08:00",
"updated_at": "2026-01-01T00:00:00+08:00",
"comment_type": "pr_comment"
},
{
"id": 100,
"body": "line note",
"user": {"login": "dev2"},
"created_at": "2026-01-02T00:00:00+08:00",
"updated_at": "2026-01-02T00:00:00+08:00",
"path": "src/main.rs",
"position": "42",
"new_line": "10",
"commit_id": "abc123",
"comment_type": "diff_comment"
}
]"#;
let mock = server
.mock("GET", api_path(path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page".into(), "1".into()),
mockito::Matcher::UrlEncoded("per_page".into(), "100".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(body)
.create();
let items = client(&server)
.pulls(&test_repo())
.list_comments(
12,
&gitee_cli_rs::api::pulls::PrCommentFilter {
limit: 30,
..Default::default()
},
)
.expect("list_comments should succeed");
mock.assert();
assert_eq!(items.len(), 2);
assert_eq!(items[0].id, 99);
assert_eq!(items[0].body, "general note");
assert_eq!(items[0].comment_type.as_deref(), Some("pr_comment"));
assert!(items[0].path.is_none());
assert_eq!(items[1].id, 100);
assert_eq!(items[1].path.as_deref(), Some("src/main.rs"));
assert_eq!(items[1].position.as_deref(), Some("42"));
assert_eq!(items[1].new_line.as_deref(), Some("10"));
assert_eq!(items[1].commit_id.as_deref(), Some("abc123"));
assert_eq!(items[1].comment_type.as_deref(), Some("diff_comment"));
}
#[test]
fn list_comments_sends_comment_type_filter() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/comments";
let body = r#"[{"id":100,"body":"line","comment_type":"diff_comment"}]"#;
let mock = server
.mock("GET", api_path(path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page".into(), "1".into()),
mockito::Matcher::UrlEncoded("per_page".into(), "100".into()),
mockito::Matcher::UrlEncoded("comment_type".into(), "diff_comment".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(body)
.create();
let items = client(&server)
.pulls(&test_repo())
.list_comments(
12,
&gitee_cli_rs::api::pulls::PrCommentFilter {
kind: Some(gitee_cli_rs::models::PrCommentKind::Diff),
limit: 30,
},
)
.expect("list_comments with type should succeed");
mock.assert();
assert_eq!(items.len(), 1);
assert_eq!(items[0].id, 100);
}
#[test]
fn latest_comment_picks_authors_most_recent_pr_comment() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/comments";
let body = r#"[
{
"id": 10,
"body": "older",
"user": {"login": "me"},
"created_at": "2026-01-01T00:00:00+08:00",
"comment_type": "pr_comment"
},
{
"id": 11,
"body": "newer",
"user": {"login": "me"},
"created_at": "2026-01-02T00:00:00+08:00",
"path": "a.rs",
"position": "3",
"comment_type": "diff_comment"
}
]"#;
let mock = server
.mock("GET", api_path(path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page".into(), "1".into()),
mockito::Matcher::UrlEncoded("per_page".into(), "100".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(body)
.create();
let comment = client(&server)
.pulls(&test_repo())
.latest_comment(12, "me")
.expect("latest_comment should succeed");
mock.assert();
assert_eq!(comment.id, 11);
assert_eq!(comment.body, "newer");
assert_eq!(comment.path.as_deref(), Some("a.rs"));
}
#[test]
fn latest_comment_errors_when_author_has_none_on_pr() {
use gitee_cli_rs::error::GiteeError;
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/comments";
let body = r#"[{"id":1,"body":"x","user":{"login":"other"},"created_at":"2026-01-01T00:00:00+08:00"}]"#;
let mock = server
.mock("GET", api_path(path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page".into(), "1".into()),
mockito::Matcher::UrlEncoded("per_page".into(), "100".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(body)
.create();
let err = client(&server)
.pulls(&test_repo())
.latest_comment(12, "me")
.expect_err("no comment by me should error");
mock.assert();
assert!(
matches!(err, GiteeError::Usage(_)),
"expected Usage error, got {err:?}"
);
}
#[test]
fn update_comment_patches_form_body_by_id() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/comments/42";
let response = r#"{"id":42,"body":"fixed review note","comment_type":"pr_comment"}"#;
let mock = server
.mock("PATCH", api_path(path).as_str())
.match_body(mockito::Matcher::UrlEncoded(
"body".into(),
"fixed review note".into(),
))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(response)
.create();
let comment = client(&server)
.pulls(&test_repo())
.update_comment(42, "fixed review note")
.expect("update_comment should succeed");
mock.assert();
assert_eq!(comment.id, 42);
assert_eq!(comment.body, "fixed review note");
}
#[test]
fn update_latest_comment_lists_then_patches_authors_most_recent() {
let mut server = mockito::Server::new();
let list_path = "/repos/oschina/gitee-cli/pulls/12/comments";
let patch_path = "/repos/oschina/gitee-cli/pulls/comments/11";
let list_body = r#"[
{
"id": 10,
"body": "older",
"user": {"login": "me"},
"created_at": "2026-01-01T00:00:00+08:00",
"comment_type": "pr_comment"
},
{
"id": 11,
"body": "newer",
"user": {"login": "me"},
"created_at": "2026-01-02T00:00:00+08:00",
"path": "a.rs",
"position": "3",
"comment_type": "diff_comment"
}
]"#;
let patch_response = r#"{"id":11,"body":"edited latest","path":"a.rs","position":"3","comment_type":"diff_comment"}"#;
let list_mock = server
.mock("GET", api_path(list_path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page".into(), "1".into()),
mockito::Matcher::UrlEncoded("per_page".into(), "100".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(list_body)
.create();
let patch_mock = server
.mock("PATCH", api_path(patch_path).as_str())
.match_body(mockito::Matcher::UrlEncoded(
"body".into(),
"edited latest".into(),
))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(patch_response)
.create();
let comment = client(&server)
.pulls(&test_repo())
.update_latest_comment(12, "me", "edited latest")
.expect("update_latest_comment should succeed");
list_mock.assert();
patch_mock.assert();
assert_eq!(comment.id, 11);
assert_eq!(comment.body, "edited latest");
}
#[test]
fn delete_comment_deletes_by_id() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/comments/42";
let mock = server
.mock("DELETE", api_path(path).as_str())
.with_status(204)
.create();
let change = client(&server)
.pulls(&test_repo())
.delete_comment(42)
.expect("delete_comment should succeed");
mock.assert();
assert!(matches!(change, StateChange::Changed(())));
}
#[test]
fn delete_comment_404_is_idempotent_ok() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/comments/42";
let mock = server
.mock("DELETE", api_path(path).as_str())
.with_status(404)
.with_header("content-type", "application/json")
.with_body(r#"{"message":"Not Found"}"#)
.create();
let change = client(&server)
.pulls(&test_repo())
.delete_comment(42)
.expect("already-gone comment should be Ok");
mock.assert();
assert!(matches!(change, StateChange::Already(())));
}
#[test]
fn delete_latest_comment_lists_then_deletes_authors_most_recent() {
let mut server = mockito::Server::new();
let list_path = "/repos/oschina/gitee-cli/pulls/12/comments";
let delete_path = "/repos/oschina/gitee-cli/pulls/comments/11";
let list_body = r#"[
{
"id": 10,
"body": "older",
"user": {"login": "me"},
"created_at": "2026-01-01T00:00:00+08:00",
"comment_type": "pr_comment"
},
{
"id": 11,
"body": "newer",
"user": {"login": "me"},
"created_at": "2026-01-02T00:00:00+08:00",
"path": "a.rs",
"position": "3",
"comment_type": "diff_comment"
}
]"#;
let list_mock = server
.mock("GET", api_path(list_path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page".into(), "1".into()),
mockito::Matcher::UrlEncoded("per_page".into(), "100".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(list_body)
.create();
let delete_mock = server
.mock("DELETE", api_path(delete_path).as_str())
.with_status(204)
.create();
let change = client(&server)
.pulls(&test_repo())
.delete_latest_comment(12, "me")
.expect("delete_latest_comment should succeed");
list_mock.assert();
delete_mock.assert();
assert!(matches!(change, StateChange::Changed(())));
}
#[test]
fn approve_force_true_sends_force_field() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/review";
let mock = server
.mock("POST", api_path(path).as_str())
.match_body(mockito::Matcher::UrlEncoded("force".into(), "true".into()))
.with_status(200)
.create();
client(&server)
.pulls(&test_repo())
.approve(12, true)
.expect("approve should succeed");
mock.assert();
}
#[test]
fn approve_force_false_sends_empty_form() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/review";
let mock = server
.mock("POST", api_path(path).as_str())
.match_body(mockito::Matcher::Exact(String::new()))
.with_status(200)
.create();
client(&server)
.pulls(&test_repo())
.approve(12, false)
.expect("approve should succeed");
mock.assert();
}
#[test]
fn test_force_true_sends_force_field() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/test";
let mock = server
.mock("POST", api_path(path).as_str())
.match_body(mockito::Matcher::UrlEncoded("force".into(), "true".into()))
.with_status(200)
.create();
client(&server)
.pulls(&test_repo())
.test(12, true)
.expect("test should succeed");
mock.assert();
}
#[test]
fn test_force_false_sends_empty_form() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/test";
let mock = server
.mock("POST", api_path(path).as_str())
.match_body(mockito::Matcher::Exact(String::new()))
.with_status(200)
.create();
client(&server)
.pulls(&test_repo())
.test(12, false)
.expect("test should succeed");
mock.assert();
}
#[test]
fn link_get_then_patch_appends_linked_tag() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12";
let get = server
.mock("GET", api_path(path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"number":12,"title":"Link me","body":"Existing body","html_url":"https://gitee.com/x"}"#,
)
.create();
let patch = server
.mock("PATCH", api_path(path).as_str())
.match_body(mockito::Matcher::Regex(r"Linked%3A\+%2342".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let linked = client(&server)
.pulls(&test_repo())
.link(12, "#42")
.expect("link should succeed");
get.assert();
patch.assert();
assert!(linked);
}
#[test]
fn link_already_linked_skips_patch() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12";
let get = server
.mock("GET", api_path(path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"number":12,"title":"Done","body":"Already Linked: #42","html_url":"https://gitee.com/x"}"#,
)
.create();
let patch = server
.mock("PATCH", api_path(path).as_str())
.expect(0)
.create();
let linked = client(&server)
.pulls(&test_repo())
.link(12, "#42")
.expect("link should short-circuit");
get.assert();
patch.assert();
assert!(!linked);
}
#[test]
fn edit_sends_only_provided_fields_comma_joined() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12";
let mock = server
.mock("PATCH", api_path(path).as_str())
.match_body(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("title".into(), "New title".into()),
mockito::Matcher::UrlEncoded("labels".into(), "bug,regression,ui".into()),
mockito::Matcher::UrlEncoded("assignees".into(), "dev1,dev2".into()),
mockito::Matcher::UrlEncoded("testers".into(), "qa1".into()),
mockito::Matcher::UrlEncoded("milestone_number".into(), "7".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let pr = client(&server)
.pulls(&test_repo())
.edit(
12,
&EditPr {
title: Some("New title"),
labels: Some("bug,regression,ui"),
assignees: Some("dev1,dev2"),
testers: Some("qa1"),
milestone_number: Some(7),
..Default::default()
},
)
.expect("edit should succeed");
mock.assert();
assert_eq!(pr.number, 12);
}
#[test]
fn edit_omits_unset_fields_from_form_body() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12";
let mock = server
.mock("PATCH", api_path(path).as_str())
.match_body(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("body".into(), "hello world".into()),
mockito::Matcher::Regex("^[^&=]+=[^&]*$".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
client(&server)
.pulls(&test_repo())
.edit(
12,
&EditPr {
body: Some("hello world"),
..Default::default()
},
)
.expect("edit should succeed");
mock.assert();
}
#[test]
fn list_milestones_hits_milestones_path() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/milestones";
let mock = server
.mock("GET", api_path(path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"[{"number":7,"title":"v1.0"},{"number":9,"title":"v2.0"}]"#)
.create();
let milestones = client(&server)
.repos()
.list_milestones("oschina", "gitee-cli")
.expect("list milestones should succeed");
mock.assert();
assert_eq!(milestones.len(), 2);
assert_eq!(milestones[0].number, 7);
assert_eq!(milestones[1].title, "v2.0");
}
#[test]
fn list_sends_assignee_and_tester_filters() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls";
let mock = server
.mock("GET", api_path(path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("state".into(), "open".into()),
mockito::Matcher::UrlEncoded("assignee".into(), "dev1".into()),
mockito::Matcher::UrlEncoded("tester".into(), "qa1".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(format!("[{PULL_REQUEST_JSON}]"))
.create();
let items = client(&server)
.pulls(&test_repo())
.list(&PrFilter {
state: Some("open"),
assignee: Some("dev1"),
tester: Some("qa1"),
limit: 50,
..Default::default()
})
.expect("list should succeed");
mock.assert();
assert_eq!(items.len(), 1);
assert_eq!(items[0].number, 12);
}
const PR_LABELS_JSON: &str = r#"[
{"id":1,"name":"bug","color":"ff0000"},
{"id":2,"name":"ui","color":"00ff00"}
]"#;
#[test]
fn list_labels_gets_paged_pr_labels_path() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/labels";
let mock = server
.mock("GET", api_path(path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page".into(), "1".into()),
mockito::Matcher::UrlEncoded("per_page".into(), "100".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_LABELS_JSON)
.create();
let items = client(&server)
.pulls(&test_repo())
.list_labels(12)
.expect("list_labels should succeed");
mock.assert();
assert_eq!(items.len(), 2);
assert_eq!(items[0].name, "bug");
assert_eq!(items[1].name, "ui");
}
#[test]
fn add_labels_idempotent_posts_only_missing_as_json_array() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/labels";
let get = server
.mock("GET", api_path(path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page".into(), "1".into()),
mockito::Matcher::UrlEncoded("per_page".into(), "100".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(r#"[{"id":1,"name":"bug","color":"ff0000"}]"#)
.create();
let post = server
.mock("POST", api_path(path).as_str())
.match_body(mockito::Matcher::Json(serde_json::json!(["ui"])))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_LABELS_JSON)
.create();
let change = client(&server)
.pulls(&test_repo())
.add_labels_idempotent(12, &["bug", "ui"])
.expect("add_labels_idempotent should succeed");
get.assert();
post.assert();
match change {
StateChange::Changed(labels) => {
assert_eq!(labels.len(), 2);
assert_eq!(labels[1].name, "ui");
}
other => panic!("expected Changed, got {other:?}"),
}
}
#[test]
fn add_labels_idempotent_already_present_skips_post() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12/labels";
let get = server
.mock("GET", api_path(path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page".into(), "1".into()),
mockito::Matcher::UrlEncoded("per_page".into(), "100".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_LABELS_JSON)
.create();
let post = server
.mock("POST", api_path(path).as_str())
.expect(0)
.create();
let change = client(&server)
.pulls(&test_repo())
.add_labels_idempotent(12, &["bug"])
.expect("add of present label should succeed");
get.assert();
post.assert();
assert!(matches!(change, StateChange::Already(_)));
}
#[test]
fn remove_labels_idempotent_deletes_only_present() {
let mut server = mockito::Server::new();
let list_path = "/repos/oschina/gitee-cli/pulls/12/labels";
let del_path = "/repos/oschina/gitee-cli/pulls/12/labels/bug";
let get = server
.mock("GET", api_path(list_path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page".into(), "1".into()),
mockito::Matcher::UrlEncoded("per_page".into(), "100".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_LABELS_JSON)
.create();
let del = server
.mock("DELETE", api_path(del_path).as_str())
.with_status(204)
.create();
let change = client(&server)
.pulls(&test_repo())
.remove_labels_idempotent(12, &["bug", "missing"])
.expect("remove_labels_idempotent should succeed");
get.assert();
del.assert();
assert!(matches!(change, StateChange::Changed(())));
}
#[test]
fn remove_labels_idempotent_absent_skips_delete() {
let mut server = mockito::Server::new();
let list_path = "/repos/oschina/gitee-cli/pulls/12/labels";
let get = server
.mock("GET", api_path(list_path).as_str())
.match_query(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("page".into(), "1".into()),
mockito::Matcher::UrlEncoded("per_page".into(), "100".into()),
]))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_LABELS_JSON)
.create();
let del = server
.mock("DELETE", mockito::Matcher::Any)
.expect(0)
.create();
let change = client(&server)
.pulls(&test_repo())
.remove_labels_idempotent(12, &["missing"])
.expect("remove of absent label should succeed");
get.assert();
del.assert();
assert!(matches!(change, StateChange::Already(())));
}
const PR_WITH_MEMBERS_JSON: &str = r#"{
"number": 12,
"title": "Add pagination helpers",
"state": "open",
"html_url": "https://gitee.com/oschina/gitee-cli/pulls/12",
"head": {"ref": "feature/paging"},
"base": {"ref": "master"},
"assignees": [
{"login": "dev1", "accept": true},
{"login": "dev2", "accept": false}
],
"testers": [
{"login": "qa1"}
]
}"#;
#[test]
fn list_assignees_reads_from_pull_request() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12";
let mock = server
.mock("GET", api_path(path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_WITH_MEMBERS_JSON)
.create();
let items = client(&server)
.pulls(&test_repo())
.list_assignees(12)
.expect("list_assignees should succeed");
mock.assert();
assert_eq!(items.len(), 2);
assert_eq!(items[0].login, "dev1");
assert_eq!(items[0].accept, Some(true));
assert_eq!(items[1].login, "dev2");
assert_eq!(items[1].accept, Some(false));
}
#[test]
fn list_testers_reads_from_pull_request() {
let mut server = mockito::Server::new();
let path = "/repos/oschina/gitee-cli/pulls/12";
let mock = server
.mock("GET", api_path(path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_WITH_MEMBERS_JSON)
.create();
let items = client(&server)
.pulls(&test_repo())
.list_testers(12)
.expect("list_testers should succeed");
mock.assert();
assert_eq!(items.len(), 1);
assert_eq!(items[0].login, "qa1");
}
#[test]
fn add_assignees_idempotent_posts_only_missing_as_form() {
let mut server = mockito::Server::new();
let get_path = "/repos/oschina/gitee-cli/pulls/12";
let post_path = "/repos/oschina/gitee-cli/pulls/12/assignees";
let get = server
.mock("GET", api_path(get_path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_WITH_MEMBERS_JSON)
.create();
let post = server
.mock("POST", api_path(post_path).as_str())
.match_body(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("assignees".into(), "dev3".into()),
]))
.with_status(201)
.with_header("content-type", "application/json")
.with_body(PR_WITH_MEMBERS_JSON)
.create();
let change = client(&server)
.pulls(&test_repo())
.add_assignees_idempotent(12, &["dev1", "dev3"])
.expect("add_assignees_idempotent should succeed");
get.assert();
post.assert();
assert!(matches!(change, StateChange::Changed(_)));
}
#[test]
fn add_assignees_idempotent_already_present_skips_post() {
let mut server = mockito::Server::new();
let get_path = "/repos/oschina/gitee-cli/pulls/12";
let post_path = "/repos/oschina/gitee-cli/pulls/12/assignees";
let get = server
.mock("GET", api_path(get_path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_WITH_MEMBERS_JSON)
.create();
let post = server
.mock("POST", api_path(post_path).as_str())
.expect(0)
.create();
let change = client(&server)
.pulls(&test_repo())
.add_assignees_idempotent(12, &["dev1"])
.expect("add of present assignee should succeed");
get.assert();
post.assert();
assert!(matches!(change, StateChange::Already(_)));
}
#[test]
fn remove_assignees_idempotent_deletes_present_via_query() {
let mut server = mockito::Server::new();
let get_path = "/repos/oschina/gitee-cli/pulls/12";
let del_path = "/repos/oschina/gitee-cli/pulls/12/assignees";
let get = server
.mock("GET", api_path(get_path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_WITH_MEMBERS_JSON)
.create();
let del = server
.mock("DELETE", api_path(del_path).as_str())
.match_query(mockito::Matcher::UrlEncoded(
"assignees".into(),
"dev2".into(),
))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let change = client(&server)
.pulls(&test_repo())
.remove_assignees_idempotent(12, &["dev2", "missing"])
.expect("remove_assignees_idempotent should succeed");
get.assert();
del.assert();
assert!(matches!(change, StateChange::Changed(())));
}
#[test]
fn remove_assignees_idempotent_absent_skips_delete() {
let mut server = mockito::Server::new();
let get_path = "/repos/oschina/gitee-cli/pulls/12";
let get = server
.mock("GET", api_path(get_path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_WITH_MEMBERS_JSON)
.create();
let del = server
.mock("DELETE", mockito::Matcher::Any)
.expect(0)
.create();
let change = client(&server)
.pulls(&test_repo())
.remove_assignees_idempotent(12, &["missing"])
.expect("remove of absent assignee should succeed");
get.assert();
del.assert();
assert!(matches!(change, StateChange::Already(())));
}
#[test]
fn add_testers_idempotent_posts_only_missing_as_form() {
let mut server = mockito::Server::new();
let get_path = "/repos/oschina/gitee-cli/pulls/12";
let post_path = "/repos/oschina/gitee-cli/pulls/12/testers";
let get = server
.mock("GET", api_path(get_path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_WITH_MEMBERS_JSON)
.create();
let post = server
.mock("POST", api_path(post_path).as_str())
.match_body(mockito::Matcher::AllOf(vec![
mockito::Matcher::UrlEncoded("testers".into(), "qa2".into()),
]))
.with_status(201)
.with_header("content-type", "application/json")
.with_body(PR_WITH_MEMBERS_JSON)
.create();
let change = client(&server)
.pulls(&test_repo())
.add_testers_idempotent(12, &["qa1", "qa2"])
.expect("add_testers_idempotent should succeed");
get.assert();
post.assert();
assert!(matches!(change, StateChange::Changed(_)));
}
#[test]
fn add_testers_idempotent_already_present_skips_post() {
let mut server = mockito::Server::new();
let get_path = "/repos/oschina/gitee-cli/pulls/12";
let post_path = "/repos/oschina/gitee-cli/pulls/12/testers";
let get = server
.mock("GET", api_path(get_path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_WITH_MEMBERS_JSON)
.create();
let post = server
.mock("POST", api_path(post_path).as_str())
.expect(0)
.create();
let change = client(&server)
.pulls(&test_repo())
.add_testers_idempotent(12, &["qa1"])
.expect("add of present tester should succeed");
get.assert();
post.assert();
assert!(matches!(change, StateChange::Already(_)));
}
#[test]
fn remove_testers_idempotent_deletes_present_via_query() {
let mut server = mockito::Server::new();
let get_path = "/repos/oschina/gitee-cli/pulls/12";
let del_path = "/repos/oschina/gitee-cli/pulls/12/testers";
let get = server
.mock("GET", api_path(get_path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_WITH_MEMBERS_JSON)
.create();
let del = server
.mock("DELETE", api_path(del_path).as_str())
.match_query(mockito::Matcher::UrlEncoded("testers".into(), "qa1".into()))
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PULL_REQUEST_JSON)
.create();
let change = client(&server)
.pulls(&test_repo())
.remove_testers_idempotent(12, &["qa1", "missing"])
.expect("remove_testers_idempotent should succeed");
get.assert();
del.assert();
assert!(matches!(change, StateChange::Changed(())));
}
#[test]
fn remove_testers_idempotent_absent_skips_delete() {
let mut server = mockito::Server::new();
let get_path = "/repos/oschina/gitee-cli/pulls/12";
let get = server
.mock("GET", api_path(get_path).as_str())
.with_status(200)
.with_header("content-type", "application/json")
.with_body(PR_WITH_MEMBERS_JSON)
.create();
let del = server
.mock("DELETE", mockito::Matcher::Any)
.expect(0)
.create();
let change = client(&server)
.pulls(&test_repo())
.remove_testers_idempotent(12, &["missing"])
.expect("remove of absent tester should succeed");
get.assert();
del.assert();
assert!(matches!(change, StateChange::Already(())));
}
#[test]
fn is_merged_true_on_204_false_on_404() {
let mut server = mockito::Server::new();
let ok = server
.mock(
"GET",
api_path("/repos/oschina/gitee-cli/pulls/12/merge").as_str(),
)
.with_status(204)
.create();
assert!(client(&server)
.pulls(&test_repo())
.is_merged(12)
.unwrap());
ok.assert();
let mut server = mockito::Server::new();
let missing = server
.mock(
"GET",
api_path("/repos/oschina/gitee-cli/pulls/12/merge").as_str(),
)
.with_status(404)
.with_body("{}")
.create();
assert!(!client(&server)
.pulls(&test_repo())
.is_merged(12)
.unwrap());
missing.assert();
}