1use super::Github;
2use crate::{
3 api_traits::{ApiOperation, CommentMergeRequest, MergeRequest, NumberDeltaErr, RemoteProject},
4 cli::browse::BrowseOptions,
5 cmds::{
6 merge_request::{
7 Comment, CommentMergeRequestBodyArgs, CommentMergeRequestListBodyArgs,
8 MergeRequestBodyArgs, MergeRequestListBodyArgs, MergeRequestResponse,
9 MergeRequestState,
10 },
11 project::MrMemberType,
12 },
13 http::{self, Body},
14 io::{HttpResponse, HttpRunner},
15 json_loads,
16 remote::query,
17};
18
19use crate::{error, Result};
20
21impl<R> Github<R> {
22 fn url_list_merge_requests(&self, args: &MergeRequestListBodyArgs) -> String {
23 let state = match args.state {
24 MergeRequestState::Opened => "open".to_string(),
25 MergeRequestState::Closed | MergeRequestState::Merged => "closed".to_string(),
28 };
29 if args.assignee.is_some() {
30 return format!(
31 "{}/issues?state={}&filter=assigned",
32 self.rest_api_basepath, state
33 );
34 } else if args.author.is_some() {
35 return format!(
36 "{}/issues?state={}&filter=created",
37 self.rest_api_basepath, state
38 );
39 }
40 format!(
41 "{}/repos/{}/pulls?state={}",
42 self.rest_api_basepath, self.path, state
43 )
44 }
45
46 fn resource_comments_metadata_url(&self, args: CommentMergeRequestListBodyArgs) -> String {
47 let url = format!(
48 "{}/repos/{}/issues/{}/comments?page=1",
49 self.rest_api_basepath, self.path, args.id
50 );
51 url
52 }
53}
54
55impl<R: HttpRunner<Response = HttpResponse>> MergeRequest for Github<R> {
56 fn open(&self, args: MergeRequestBodyArgs) -> Result<MergeRequestResponse> {
57 let mut body = Body::new();
59 body.add("base", args.target_branch);
60 body.add("title", args.title);
61 body.add("body", args.description);
62 if args.draft {
65 body.add("draft", args.draft.to_string());
66 }
67 let target_repo = args.target_repo.clone();
68 let mut mr_url = format!(
69 "{}/repos/{}/pulls",
70 self.rest_api_basepath,
71 self.path.clone()
72 );
73 if !target_repo.is_empty() {
74 mr_url = format!(
75 "{}/repos/{}/pulls",
76 self.rest_api_basepath, args.target_repo
77 );
78 let owner_path = self.path.split('/').collect::<Vec<&str>>();
79 if owner_path.len() != 2 {
80 return Err(error::GRError::ApplicationError(format!(
81 "Invalid path format in git config: [{}] while attempting \
82 to retrieve existing pull request. Expected owner/repo",
83 self.path
84 ))
85 .into());
86 }
87 let remote_pr_branch = format!("{}:{}", owner_path[0], args.source_branch.clone());
88 body.add("head", remote_pr_branch);
89 } else {
90 body.add("head", args.source_branch.clone());
91 }
92
93 match query::send_raw(
94 &self.runner,
95 &mr_url,
96 Some(&body),
97 self.request_headers(),
98 ApiOperation::MergeRequest,
99 http::Method::POST,
100 ) {
101 Ok(response) => {
102 match response.status {
103 201 => {
104 let body = response.body;
115 let merge_request_json = json_loads(&body)?;
116 let id = merge_request_json["number"].as_i64().unwrap();
117 match args.assignee.mr_member_type {
118 MrMemberType::Empty => (),
119 MrMemberType::Filled => {
120 let issues_url = format!(
123 "{}/repos/{}/issues/{}",
124 self.rest_api_basepath, self.path, id
125 );
126 let mut body = Body::new();
127 let assignees = vec![args.assignee.username.as_str()];
128 body.add("assignees", &assignees);
129 query::send_raw(
130 &self.runner,
131 &issues_url,
132 Some(&body),
133 self.request_headers(),
134 ApiOperation::MergeRequest,
135 http::Method::PATCH,
136 )?;
137 }
138 }
139 match args.reviewer.mr_member_type {
142 MrMemberType::Empty => (),
143 MrMemberType::Filled => {
144 let mut body = Body::new();
145 let reviewers = vec![args.reviewer.username.as_str()];
146 body.add("reviewers", &reviewers);
147 let requested_reviewers_url =
148 format!("{mr_url}/{id}/requested_reviewers");
149
150 let response = query::send_raw(
151 &self.runner,
152 &requested_reviewers_url,
153 Some(&body),
154 self.request_headers(),
155 ApiOperation::MergeRequest,
156 http::Method::POST,
157 )?;
158 if response.status != 201 {
160 return Err(query::query_error(
161 &requested_reviewers_url,
162 &response,
163 )
164 .into());
165 }
166 }
167 }
168 Ok(GithubMergeRequestFields::from(&merge_request_json).into())
169 }
170 422 => {
171 let owner_path = self.path.split('/').collect::<Vec<&str>>();
179 if owner_path.len() != 2 {
180 return Err(error::GRError::ApplicationError(format!(
181 "Invalid path format in git config: [{}] while attempting \
182 to retrieve existing pull request. Expected owner/repo",
183 self.path
184 ))
185 .into());
186 }
187 let remote_pr_branch = format!("{}:{}", owner_path[0], args.source_branch);
188 let existing_mr_url = format!("{mr_url}?head={remote_pr_branch}");
189 let response = query::get_raw::<_, ()>(
190 &self.runner,
191 &existing_mr_url,
192 None,
193 self.request_headers(),
194 ApiOperation::MergeRequest,
195 )?;
196 let merge_requests_json: Vec<serde_json::Value> =
197 serde_json::from_str(&response.body)?;
198 if merge_requests_json.len() == 1 {
199 let mr_id = merge_requests_json[0]["number"].as_i64().unwrap();
200 if args.amend {
201 let url = format!(
203 "{}/repos/{}/pulls/{}",
204 self.rest_api_basepath, self.path, mr_id
205 );
206 query::send_json::<_, String>(
207 &self.runner,
208 &url,
209 Some(&body),
210 self.request_headers(),
211 ApiOperation::MergeRequest,
212 http::Method::PATCH,
213 )?;
214 }
215 Ok(MergeRequestResponse::builder()
216 .id(mr_id)
217 .web_url(
218 merge_requests_json[0]["html_url"]
219 .to_string()
220 .trim_matches('"')
221 .to_string(),
222 )
223 .build()
224 .unwrap())
225 } else {
226 Err(error::GRError::RemoteUnexpectedResponseContract(format!(
227 "There should have been an existing pull request at \
228 URL: {} but got an unexpected response: {}",
229 existing_mr_url, response.body
230 ))
231 .into())
232 }
233 }
234 _ => Err(error::gen(format!(
235 "Failed to create merge request. Status code: {}, Body: {}",
236 response.status, response.body
237 ))),
238 }
239 }
240 Err(err) => Err(err),
241 }
242 }
243
244 fn list(&self, args: MergeRequestListBodyArgs) -> Result<Vec<MergeRequestResponse>> {
245 let url = self.url_list_merge_requests(&args);
246 let response = query::paged::<_, MergeRequestResponse>(
247 &self.runner,
248 &url,
249 args.list_args,
250 self.request_headers(),
251 None,
252 ApiOperation::MergeRequest,
253 |value| GithubMergeRequestFields::from(value).into(),
254 );
255 if args.assignee.is_some() || args.author.is_some() {
256 let mut merge_requests = vec![];
267 for mr in response? {
268 if !mr.pull_request.is_empty() {
269 merge_requests.push(mr);
270 }
271 }
272 return Ok(merge_requests);
273 }
274 response
275 }
276
277 fn merge(&self, id: i64) -> Result<MergeRequestResponse> {
278 let url = format!(
281 "{}/repos/{}/pulls/{}/merge",
282 self.rest_api_basepath, self.path, id
283 );
284 query::send_json::<_, ()>(
285 &self.runner,
286 &url,
287 None,
288 self.request_headers(),
289 ApiOperation::MergeRequest,
290 http::Method::PUT,
291 )?;
292 Ok(MergeRequestResponse::builder()
302 .id(id)
303 .web_url(self.get_url(BrowseOptions::MergeRequestId(id)))
304 .build()
305 .unwrap())
306 }
307
308 fn get(&self, id: i64) -> Result<MergeRequestResponse> {
309 let url = format!(
310 "{}/repos/{}/pulls/{}",
311 self.rest_api_basepath, self.path, id
312 );
313 query::get::<_, (), _>(
314 &self.runner,
315 &url,
316 None,
317 self.request_headers(),
318 ApiOperation::MergeRequest,
319 |value| GithubMergeRequestFields::from(value).into(),
320 )
321 }
322
323 fn close(&self, id: i64) -> Result<MergeRequestResponse> {
324 let url = format!(
325 "{}/repos/{}/pulls/{}",
326 self.rest_api_basepath, self.path, id
327 );
328 let mut body = Body::new();
329 body.add("state", "closed");
330 query::send::<_, &str, _>(
331 &self.runner,
332 &url,
333 Some(&body),
334 self.request_headers(),
335 ApiOperation::MergeRequest,
336 |value| GithubMergeRequestFields::from(value).into(),
337 http::Method::PATCH,
338 )
339 }
340
341 fn num_pages(&self, args: MergeRequestListBodyArgs) -> Result<Option<u32>> {
342 let url = self.url_list_merge_requests(&args) + "&page=1";
343 let headers = self.request_headers();
344 query::num_pages(&self.runner, &url, headers, ApiOperation::MergeRequest)
345 }
346
347 fn num_resources(&self, args: MergeRequestListBodyArgs) -> Result<Option<NumberDeltaErr>> {
348 let url = self.url_list_merge_requests(&args) + "&page=1";
349 let headers = self.request_headers();
350 query::num_resources(&self.runner, &url, headers, ApiOperation::MergeRequest)
351 }
352
353 fn approve(&self, _id: i64) -> Result<MergeRequestResponse> {
354 todo!()
355 }
356}
357
358impl<R: HttpRunner<Response = HttpResponse>> CommentMergeRequest for Github<R> {
359 fn create(&self, args: CommentMergeRequestBodyArgs) -> Result<()> {
360 let url = format!(
361 "{}/repos/{}/issues/{}/comments",
362 self.rest_api_basepath, self.path, args.id
363 );
364 let mut body = Body::new();
365 body.add("body", args.comment);
366 query::send_raw(
367 &self.runner,
368 &url,
369 Some(&body),
370 self.request_headers(),
371 ApiOperation::MergeRequest,
372 http::Method::POST,
373 )?;
374 Ok(())
375 }
376
377 fn list(&self, args: CommentMergeRequestListBodyArgs) -> Result<Vec<Comment>> {
378 let url = format!(
379 "{}/repos/{}/issues/{}/comments",
380 self.rest_api_basepath, self.path, args.id
381 );
382 query::paged(
383 &self.runner,
384 &url,
385 args.list_args,
386 self.request_headers(),
387 None,
388 ApiOperation::MergeRequest,
389 |value| GithubMergeRequestCommentFields::from(value).into(),
390 )
391 }
392
393 fn num_pages(&self, args: CommentMergeRequestListBodyArgs) -> Result<Option<u32>> {
394 let url = self.resource_comments_metadata_url(args);
395 query::num_pages(
396 &self.runner,
397 &url,
398 self.request_headers(),
399 ApiOperation::MergeRequest,
400 )
401 }
402
403 fn num_resources(
404 &self,
405 args: CommentMergeRequestListBodyArgs,
406 ) -> Result<Option<NumberDeltaErr>> {
407 let url = self.resource_comments_metadata_url(args);
408 query::num_resources(
409 &self.runner,
410 &url,
411 self.request_headers(),
412 ApiOperation::MergeRequest,
413 )
414 }
415}
416
417pub struct GithubMergeRequestFields {
418 fields: MergeRequestResponse,
419}
420
421impl From<&serde_json::Value> for GithubMergeRequestFields {
422 fn from(merge_request_data: &serde_json::Value) -> Self {
423 GithubMergeRequestFields {
424 fields: MergeRequestResponse::builder()
425 .id(merge_request_data["number"].as_i64().unwrap())
426 .web_url(merge_request_data["html_url"].as_str().unwrap().to_string())
427 .source_branch(
428 merge_request_data["head"]["ref"]
429 .as_str()
430 .unwrap_or_default()
431 .to_string(),
432 )
433 .sha(
434 merge_request_data["merge_commit_sha"]
435 .as_str()
436 .unwrap_or_default()
437 .to_string(),
438 )
439 .author(
440 merge_request_data["user"]["login"]
441 .as_str()
442 .unwrap_or_default()
443 .to_string(),
444 )
445 .updated_at(
446 merge_request_data["updated_at"]
447 .as_str()
448 .unwrap_or_default()
449 .to_string(),
450 )
451 .created_at(
452 merge_request_data["created_at"]
453 .as_str()
454 .unwrap_or_default()
455 .to_string(),
456 )
457 .title(
458 merge_request_data["title"]
459 .as_str()
460 .unwrap_or_default()
461 .to_string(),
462 )
463 .pull_request(
464 merge_request_data["pull_request"]["html_url"]
465 .as_str()
466 .unwrap_or_default()
467 .to_string(),
468 )
469 .description(
470 merge_request_data["body"]
471 .as_str()
472 .unwrap_or_default()
473 .to_string(),
474 )
475 .merged_at(
476 merge_request_data["merged_at"]
477 .as_str()
478 .unwrap_or_default()
479 .to_string(),
480 )
481 .pipeline_id(Some(merge_request_data["number"].as_i64().unwrap()))
483 .pipeline_url(
484 merge_request_data["html_url"]
485 .as_str()
486 .map(|url| format!("{url}/checks")),
487 )
488 .build()
489 .unwrap(),
490 }
491 }
492}
493
494impl From<GithubMergeRequestFields> for MergeRequestResponse {
495 fn from(fields: GithubMergeRequestFields) -> Self {
496 fields.fields
497 }
498}
499
500pub struct GithubMergeRequestCommentFields {
501 comment: Comment,
502}
503
504impl From<&serde_json::Value> for GithubMergeRequestCommentFields {
505 fn from(comment_data: &serde_json::Value) -> Self {
506 GithubMergeRequestCommentFields {
507 comment: Comment::builder()
508 .id(comment_data["id"].as_i64().unwrap())
509 .author(comment_data["user"]["login"].as_str().unwrap().to_string())
510 .created_at(comment_data["created_at"].as_str().unwrap().to_string())
511 .body(comment_data["body"].as_str().unwrap().to_string())
512 .build()
513 .unwrap(),
514 }
515 }
516}
517
518impl From<GithubMergeRequestCommentFields> for Comment {
519 fn from(fields: GithubMergeRequestCommentFields) -> Self {
520 fields.comment
521 }
522}
523
524#[cfg(test)]
525mod test {
526
527 use crate::{
528 cmds::project::{Member, MrMemberType},
529 http::{self, Headers},
530 remote::ListBodyArgs,
531 setup_client,
532 test::utils::{
533 default_github, get_contract, BasePath, ClientType, ContractType, Domain,
534 ResponseContracts,
535 },
536 };
537
538 use super::*;
539
540 #[test]
541 fn test_open_merge_request() {
542 let responses = ResponseContracts::new(ContractType::Github)
543 .add_contract(201, "merge_request.json", None)
544 .add_contract(200, "merge_request.json", None)
545 .add_contract(201, "merge_request.json", None);
546 let (client, github) = setup_client!(responses, default_github(), dyn MergeRequest);
547 let assignee = Member::builder()
548 .name("tom".to_string())
549 .username("tsawyer".to_string())
550 .mr_member_type(MrMemberType::Filled)
551 .id(1234)
552 .build()
553 .unwrap();
554 let reviewer = Member::builder()
555 .name("huck".to_string())
556 .username("hfinn".to_string())
557 .mr_member_type(MrMemberType::Filled)
558 .id(45678)
559 .build()
560 .unwrap();
561 let mr_args = MergeRequestBodyArgs::builder()
562 .assignee(assignee)
563 .reviewer(reviewer)
564 .build()
565 .unwrap();
566 let response = github.open(mr_args).unwrap();
567 assert_eq!(23, response.id);
568 assert_eq!(
569 "https://github.com/jordilin/githapi/pull/23",
570 response.web_url
571 );
572 assert_eq!(
573 "https://api.github.com/repos/jordilin/githapi/pulls/23/requested_reviewers",
574 *client.url(),
575 );
576 let actual_method = client.http_method.borrow();
577 assert_eq!(http::Method::POST, actual_method[0]);
578 assert_eq!(http::Method::PATCH, actual_method[1]);
580 assert_eq!(http::Method::POST, actual_method[2]);
582 assert_eq!(
583 Some(ApiOperation::MergeRequest),
584 *client.api_operation.borrow()
585 );
586 }
587
588 #[test]
589 fn test_open_merge_request_with_assignee_no_reviewer() {
590 let responses = ResponseContracts::new(ContractType::Github)
591 .add_contract(200, "merge_request.json", None)
592 .add_contract(201, "merge_request.json", None);
593 let (client, github) = setup_client!(responses, default_github(), dyn MergeRequest);
594 let assignee = Member::builder()
595 .name("tom".to_string())
596 .username("tsawyer".to_string())
597 .mr_member_type(MrMemberType::Filled)
598 .id(1234)
599 .build()
600 .unwrap();
601 let reviewer = Member::default(); let mr_args = MergeRequestBodyArgs::builder()
603 .assignee(assignee)
604 .reviewer(reviewer)
605 .build()
606 .unwrap();
607 let response = github.open(mr_args).unwrap();
608 assert_eq!(23, response.id);
609 assert_eq!(
610 "https://github.com/jordilin/githapi/pull/23",
611 response.web_url
612 );
613 assert_eq!(
614 "https://api.github.com/repos/jordilin/githapi/issues/23",
615 *client.url(),
616 );
617 let actual_method = client.http_method.borrow();
618 assert_eq!(http::Method::PATCH, actual_method[1]);
619 assert_eq!(http::Method::POST, actual_method[0]);
621 assert_eq!(
622 Some(ApiOperation::MergeRequest),
623 *client.api_operation.borrow()
624 );
625 }
626
627 #[test]
628 fn test_open_merge_request_with_reviewer_no_assignee() {
629 let responses = ResponseContracts::new(ContractType::Github)
630 .add_contract(201, "merge_request.json", None)
631 .add_contract(201, "merge_request.json", None);
632 let (client, github) = setup_client!(responses, default_github(), dyn MergeRequest);
633 let assignee = Member::default();
634 let reviewer = Member::builder()
635 .name("huck".to_string())
636 .username("hfinn".to_string())
637 .mr_member_type(MrMemberType::Filled)
638 .id(1234)
639 .build()
640 .unwrap();
641 let mr_args = MergeRequestBodyArgs::builder()
642 .assignee(assignee)
643 .reviewer(reviewer)
644 .build()
645 .unwrap();
646 assert!(github.open(mr_args).is_ok());
647 assert_eq!(
648 "https://api.github.com/repos/jordilin/githapi/pulls/23/requested_reviewers",
649 *client.url(),
650 );
651 let actual_method = client.http_method.borrow();
652 assert_eq!(http::Method::POST, actual_method[1]);
654 assert_eq!(http::Method::POST, actual_method[0]);
656 assert_eq!(
657 Some(ApiOperation::MergeRequest),
658 *client.api_operation.borrow()
659 );
660 }
661
662 #[test]
663 fn test_open_merge_request_with_no_assignee_no_reviewer() {
664 let responses = ResponseContracts::new(ContractType::Github).add_contract(
665 201,
666 "merge_request.json",
667 None,
668 );
669 let (client, github) = setup_client!(responses, default_github(), dyn MergeRequest);
670 let assignee = Member::default();
671 let reviewer = Member::default();
672 let mr_args = MergeRequestBodyArgs::builder()
673 .assignee(assignee)
674 .reviewer(reviewer)
675 .build()
676 .unwrap();
677 assert!(github.open(mr_args).is_ok());
678 assert_eq!(
679 "https://api.github.com/repos/jordilin/githapi/pulls",
680 *client.url(),
681 );
682 let actual_method = client.http_method.borrow();
683 assert_eq!(http::Method::POST, actual_method[0]);
685 assert_eq!(
686 Some(ApiOperation::MergeRequest),
687 *client.api_operation.borrow()
688 );
689 }
690
691 #[test]
692 fn test_open_merge_request_on_target_repository() {
693 let mr_args = MergeRequestBodyArgs::builder()
694 .target_repo("jordilin/gitar".to_string())
695 .build()
696 .unwrap();
697 let responses = ResponseContracts::new(ContractType::Github)
698 .add_contract(200, "merge_request.json", None)
699 .add_contract(201, "merge_request.json", None);
700 let client_type = ClientType::Github(
702 Domain("github.com".to_string()),
703 BasePath("jdoe/gitar".to_string()),
704 );
705 let (client, github) = setup_client!(responses, client_type, dyn MergeRequest);
706
707 assert!(github.open(mr_args).is_ok());
708 assert_eq!(
709 "https://api.github.com/repos/jordilin/gitar/pulls",
710 *client.url(),
711 );
712 assert_eq!(
713 Some(ApiOperation::MergeRequest),
714 *client.api_operation.borrow()
715 );
716 }
717
718 #[test]
719 fn test_open_merge_request_with_no_assignee() {
720 let responses = ResponseContracts::new(ContractType::Github)
721 .add_contract(200, "merge_request.json", None)
722 .add_contract(201, "merge_request.json", None);
723 let (client, github) = setup_client!(responses, default_github(), dyn MergeRequest);
724 let assignee = Member::default();
725 let mr_args = MergeRequestBodyArgs::builder()
726 .assignee(assignee)
727 .build()
728 .unwrap();
729 assert!(github.open(mr_args).is_ok());
730 assert_eq!(
731 "https://api.github.com/repos/jordilin/githapi/pulls",
732 *client.url(),
733 );
734 let actual_method = client.http_method.borrow();
735 assert_eq!(http::Method::POST, actual_method[0]);
736 assert_eq!(
737 Some(ApiOperation::MergeRequest),
738 *client.api_operation.borrow()
739 );
740 let actual_body = client.request_body.borrow();
741 assert!(!actual_body.contains("assignees"));
742 }
743
744 #[test]
745 fn test_open_merge_request_error_status_code() {
746 let mr_args = MergeRequestBodyArgs::builder().build().unwrap();
747 let responses = ResponseContracts::new(ContractType::Github).add_body(
748 401,
749 Some(r#"{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"}"#),
750 None,
751 );
752 let (_, github) = setup_client!(responses, default_github(), dyn MergeRequest);
753 assert!(github.open(mr_args).is_err());
754 }
755
756 #[test]
757 fn test_open_merge_request_existing_one() {
758 let mr_args = MergeRequestBodyArgs::builder()
759 .source_branch("feature".to_string())
760 .build()
761 .unwrap();
762 let contracts = ResponseContracts::new(ContractType::Github)
763 .add_body(
764 200,
765 Some(format!(
766 "[{}]",
767 get_contract(ContractType::Github, "merge_request.json")
768 )),
769 None,
770 )
771 .add_contract(422, "merge_request_conflict.json", None);
775 let (client, github) = setup_client!(contracts, default_github(), dyn MergeRequest);
776
777 github.open(mr_args).unwrap();
778 assert_eq!(
779 "https://api.github.com/repos/jordilin/githapi/pulls?head=jordilin:feature",
780 *client.url(),
781 );
782 let actual_method = client.http_method.borrow();
783 assert_eq!(http::Method::GET, actual_method[1]);
784 assert_eq!(
785 Some(ApiOperation::MergeRequest),
786 *client.api_operation.borrow()
787 );
788 }
789
790 #[test]
791 fn test_amend_existing_pull_request() {
792 let mr_args = MergeRequestBodyArgs::builder()
793 .source_branch("feature".to_string())
794 .amend(true)
795 .build()
796 .unwrap();
797 let contracts = ResponseContracts::new(ContractType::Github)
798 .add_contract(200, "merge_request.json", None)
799 .add_body(
800 200,
801 Some(format!(
802 "[{}]",
803 get_contract(ContractType::Github, "merge_request.json")
804 )),
805 None,
806 )
807 .add_contract(422, "merge_request_conflict.json", None);
808
809 let (client, github) = setup_client!(contracts, default_github(), dyn MergeRequest);
810
811 github.open(mr_args).unwrap();
812 assert_eq!(
813 "https://api.github.com/repos/jordilin/githapi/pulls/23",
814 *client.url(),
815 );
816 let actual_method = client.http_method.borrow();
817 assert_eq!(http::Method::PATCH, actual_method[2]);
818 assert_eq!(
819 Some(ApiOperation::MergeRequest),
820 *client.api_operation.borrow()
821 );
822 }
823
824 #[test]
825 fn test_open_merge_request_cannot_retrieve_url_existing_one_is_error() {
826 let mr_args = MergeRequestBodyArgs::builder()
827 .source_branch("feature".to_string())
828 .build()
829 .unwrap();
830 let contracts = ResponseContracts::new(ContractType::Github)
831 .add_body(200, Some("[]"), None)
832 .add_contract(422, "merge_request_conflict.json", None);
833 let (_, github) = setup_client!(contracts, default_github(), dyn MergeRequest);
834 let result = github.open(mr_args);
835 match result {
836 Ok(_) => panic!("Expected error"),
837 Err(err) => match err.downcast_ref::<error::GRError>() {
838 Some(error::GRError::RemoteUnexpectedResponseContract(_)) => (),
839 _ => panic!("Expected error::GRError::RemoteUnexpectedResponseContract"),
840 },
841 }
842 }
843
844 #[test]
845 fn test_open_merge_request_cannot_get_owner_org_namespace_in_existing_pull_request() {
846 let mr_args = MergeRequestBodyArgs::builder()
847 .source_branch("feature".to_string())
848 .build()
849 .unwrap();
850 let contracts = ResponseContracts::new(ContractType::Github)
851 .add_body(200, Some("[]"), None)
852 .add_contract(422, "merge_request_conflict.json", None);
853 let client_type = ClientType::Github(
855 Domain("github.com".to_string()),
856 BasePath("jordilin".to_string()),
857 );
858 let (_, github) = setup_client!(contracts, client_type, dyn MergeRequest);
859
860 let result = github.open(mr_args);
861 match result {
862 Ok(_) => panic!("Expected error"),
863 Err(err) => match err.downcast_ref::<error::GRError>() {
864 Some(error::GRError::ApplicationError(_)) => (),
865 _ => panic!("Expected error::GRError::ApplicationError"),
866 },
867 }
868 }
869
870 #[test]
871 fn test_merge_request_num_pages() {
872 let link_header = r#"<https://api.github.com/repos/jordilin/githapi/pulls?state=open&page=2>; rel="next", <https://api.github.com/repos/jordilin/githapi/pulls?state=open&page=2>; rel="last""#;
873 let mut headers = Headers::new();
874 headers.set("link".to_string(), link_header.to_string());
875 let contracts = ResponseContracts::new(ContractType::Github).add_body::<String>(
876 200,
877 None,
878 Some(headers),
879 );
880 let (client, github) = setup_client!(contracts, default_github(), dyn MergeRequest);
881 let args = MergeRequestListBodyArgs::builder()
882 .state(MergeRequestState::Opened)
883 .list_args(None)
884 .assignee(None)
885 .build()
886 .unwrap();
887 assert_eq!(Some(2), github.num_pages(args).unwrap());
888 assert_eq!(
889 "https://api.github.com/repos/jordilin/githapi/pulls?state=open&page=1",
890 *client.url(),
891 );
892 assert_eq!(
893 Some(ApiOperation::MergeRequest),
894 *client.api_operation.borrow()
895 );
896 }
897
898 #[test]
899 fn test_list_merge_requests_from_to_page_set_in_url() {
900 let contracts =
901 ResponseContracts::new(ContractType::Github).add_body(200, Some("[]"), None);
902
903 let (client, github) = setup_client!(contracts, default_github(), dyn MergeRequest);
904 let args = MergeRequestListBodyArgs::builder()
905 .state(MergeRequestState::Opened)
906 .list_args(Some(
907 ListBodyArgs::builder()
908 .page(2)
909 .max_pages(3)
910 .build()
911 .unwrap(),
912 ))
913 .assignee(None)
914 .build()
915 .unwrap();
916 github.list(args).unwrap();
917 assert_eq!(
918 "https://api.github.com/repos/jordilin/githapi/pulls?state=open&page=2",
919 *client.url(),
920 );
921 assert_eq!(
922 Some(ApiOperation::MergeRequest),
923 *client.api_operation.borrow()
924 );
925 }
926
927 #[test]
928 fn test_get_pull_requests_for_auth_user_is_assignee() {
929 let contracts = ResponseContracts::new(ContractType::Github).add_contract(
930 200,
931 "list_issues_user.json",
932 None,
933 );
934 let (client, github) = setup_client!(contracts, default_github(), dyn MergeRequest);
935 let args = MergeRequestListBodyArgs::builder()
936 .state(MergeRequestState::Opened)
937 .list_args(None)
938 .assignee(Some(
939 Member::builder()
940 .name("tom".to_string())
941 .username("tsawyer".to_string())
942 .id(123456)
943 .build()
944 .unwrap(),
945 ))
946 .build()
947 .unwrap();
948 let merge_requests = github.list(args).unwrap();
949 assert_eq!(
950 "https://api.github.com/issues?state=open&filter=assigned",
951 *client.url()
952 );
953 assert!(merge_requests.len() == 1);
954 assert_eq!(
955 Some(ApiOperation::MergeRequest),
956 *client.api_operation.borrow()
957 );
958 }
959
960 #[test]
961 fn test_get_pull_requests_for_auth_user_is_author() {
962 let contracts = ResponseContracts::new(ContractType::Github).add_contract(
963 200,
964 "list_issues_user.json",
965 None,
966 );
967 let (client, github) = setup_client!(contracts, default_github(), dyn MergeRequest);
968 let args = MergeRequestListBodyArgs::builder()
969 .state(MergeRequestState::Opened)
970 .list_args(None)
971 .author(Some(
972 Member::builder()
973 .name("tom".to_string())
974 .username("tsawyer".to_string())
975 .id(12345)
976 .build()
977 .unwrap(),
978 ))
979 .build()
980 .unwrap();
981 let merge_requests = github.list(args).unwrap();
982 assert_eq!(
983 "https://api.github.com/issues?state=open&filter=created",
984 *client.url()
985 );
986 assert!(merge_requests.len() == 1);
987 assert_eq!(
988 Some(ApiOperation::MergeRequest),
989 *client.api_operation.borrow()
990 );
991 }
992
993 #[test]
994 fn test_create_merge_request_comment() {
995 let contracts =
996 ResponseContracts::new(ContractType::Github).add_body::<String>(201, None, None);
997 let (client, github) = setup_client!(contracts, default_github(), dyn CommentMergeRequest);
998 let args = CommentMergeRequestBodyArgs::builder()
999 .id(23)
1000 .comment("Looks good to me".to_string())
1001 .build()
1002 .unwrap();
1003 github.create(args).unwrap();
1004 assert_eq!(
1005 "https://api.github.com/repos/jordilin/githapi/issues/23/comments",
1006 *client.url(),
1007 );
1008 assert_eq!(
1009 Some(ApiOperation::MergeRequest),
1010 *client.api_operation.borrow()
1011 );
1012 }
1013
1014 #[test]
1015 fn test_create_merge_request_comment_error_status_code() {
1016 let contracts =
1017 ResponseContracts::new(ContractType::Github).add_body::<String>(500, None, None);
1018 let (_, github) = setup_client!(contracts, default_github(), dyn CommentMergeRequest);
1019 let args = CommentMergeRequestBodyArgs::builder()
1020 .id(23)
1021 .comment("Looks good to me".to_string())
1022 .build()
1023 .unwrap();
1024 assert!(github.create(args).is_err());
1025 }
1026
1027 #[test]
1028 fn test_close_pull_request_ok() {
1029 let contracts = ResponseContracts::new(ContractType::Github).add_contract(
1030 200,
1031 "merge_request.json",
1032 None,
1033 );
1034 let (client, github) = setup_client!(contracts, default_github(), dyn MergeRequest);
1035 github.close(23).unwrap();
1036 assert_eq!(
1037 "https://api.github.com/repos/jordilin/githapi/pulls/23",
1038 *client.url(),
1039 );
1040 let actual_method = client.http_method.borrow();
1041 assert_eq!(http::Method::PATCH, actual_method[0]);
1042 assert_eq!(
1043 Some(ApiOperation::MergeRequest),
1044 *client.api_operation.borrow()
1045 );
1046 }
1047
1048 #[test]
1049 fn test_get_pull_request_details() {
1050 let contracts = ResponseContracts::new(ContractType::Github).add_contract(
1051 200,
1052 "merge_request.json",
1053 None,
1054 );
1055 let (client, github) = setup_client!(contracts, default_github(), dyn MergeRequest);
1056 github.get(23).unwrap();
1057 assert_eq!(
1058 "https://api.github.com/repos/jordilin/githapi/pulls/23",
1059 *client.url(),
1060 );
1061 assert_eq!(
1062 Some(ApiOperation::MergeRequest),
1063 *client.api_operation.borrow()
1064 );
1065 }
1066
1067 #[test]
1068 fn test_github_merge_pull_request() {
1069 let contracts = ResponseContracts::new(ContractType::Github).add_contract(
1070 200,
1071 "merge_request.json",
1072 None,
1073 );
1074 let (client, github) = setup_client!(contracts, default_github(), dyn MergeRequest);
1075 github.merge(23).unwrap();
1076 assert_eq!(
1077 "https://api.github.com/repos/jordilin/githapi/pulls/23/merge",
1078 *client.url(),
1079 );
1080 let actual_method = client.http_method.borrow();
1081 assert_eq!(http::Method::PUT, actual_method[0]);
1082 assert_eq!(
1083 Some(ApiOperation::MergeRequest),
1084 *client.api_operation.borrow()
1085 );
1086 }
1087
1088 #[test]
1089 fn test_list_pull_request_comments() {
1090 let contracts = ResponseContracts::new(ContractType::Github).add_body(
1091 200,
1092 Some(format!(
1093 "[{}]",
1094 get_contract(ContractType::Github, "comment.json")
1095 )),
1096 None,
1097 );
1098 let (client, github) = setup_client!(contracts, default_github(), dyn CommentMergeRequest);
1099 let args = CommentMergeRequestListBodyArgs::builder()
1100 .id(23)
1101 .list_args(None)
1102 .build()
1103 .unwrap();
1104 github.list(args).unwrap();
1105 assert_eq!(
1106 "https://api.github.com/repos/jordilin/githapi/issues/23/comments",
1107 *client.url(),
1108 );
1109 assert_eq!(
1110 Some(ApiOperation::MergeRequest),
1111 *client.api_operation.borrow()
1112 );
1113 }
1114
1115 #[test]
1116 fn test_pull_request_comment_num_pages() {
1117 let link_header = r#"<https://api.github.com/repos/jordilin/githapi/issues/23/comments?page=2>; rel="next", <https://api.github.com/repos/jordilin/githapi/issues/23/comments?page=2>; rel="last""#;
1118 let mut headers = Headers::new();
1119 headers.set("link".to_string(), link_header.to_string());
1120 let contracts = ResponseContracts::new(ContractType::Github).add_body::<String>(
1121 200,
1122 None,
1123 Some(headers),
1124 );
1125 let (client, github) = setup_client!(contracts, default_github(), dyn CommentMergeRequest);
1126 let args = CommentMergeRequestListBodyArgs::builder()
1127 .id(23)
1128 .list_args(None)
1129 .build()
1130 .unwrap();
1131 assert_eq!(Some(2), github.num_pages(args).unwrap());
1132 assert_eq!(
1133 "https://api.github.com/repos/jordilin/githapi/issues/23/comments?page=1",
1134 *client.url(),
1135 );
1136 assert_eq!(
1137 Some(ApiOperation::MergeRequest),
1138 *client.api_operation.borrow()
1139 );
1140 }
1141}