gr/gitlab/
merge_request.rs

1use crate::api_traits::{ApiOperation, CommentMergeRequest, NumberDeltaErr, RemoteProject};
2use crate::cli::browse::BrowseOptions;
3use crate::cmds::merge_request::{
4    Comment, CommentMergeRequestBodyArgs, CommentMergeRequestListBodyArgs, MergeRequestBodyArgs,
5    MergeRequestListBodyArgs, MergeRequestResponse,
6};
7use crate::cmds::project::MrMemberType;
8use crate::error::{self, GRError};
9use crate::http::{self, Body, Headers};
10use crate::io::CmdInfo;
11use crate::remote::query;
12use crate::Result;
13use crate::{
14    api_traits::MergeRequest,
15    io::{HttpResponse, HttpRunner},
16};
17
18use crate::json_loads;
19
20use super::Gitlab;
21
22impl<R: HttpRunner<Response = HttpResponse>> MergeRequest for Gitlab<R> {
23    fn open(&self, args: MergeRequestBodyArgs) -> Result<MergeRequestResponse> {
24        let mut body = Body::new();
25        body.add("source_branch", args.source_branch);
26        body.add("target_branch", args.target_branch);
27        body.add("title", args.title);
28        match args.assignee.mr_member_type {
29            MrMemberType::Filled => {
30                body.add("assignee_id", args.assignee.id.to_string());
31            }
32            MrMemberType::Empty => {}
33        }
34        match args.reviewer.mr_member_type {
35            MrMemberType::Filled => {
36                // We support one reviewer for now. The CE edition of Gitlab
37                // only supports one, so we'll keep it simple.
38                body.add("reviewer_ids", args.reviewer.id.to_string());
39            }
40            MrMemberType::Empty => {}
41        }
42        body.add("description", args.description);
43        body.add("remove_source_branch", args.remove_source_branch);
44        // if target repo provided, add target_project_id in the payload
45        if !args.target_repo.is_empty() {
46            match self.get_project_data(None, Some(&args.target_repo)) {
47                Ok(CmdInfo::Project(project)) => {
48                    body.add("target_project_id", project.id.to_string());
49                }
50                Ok(_) => {
51                    // Application error - any other CmdInfo variant is unexpected
52                    return Err(GRError::ApplicationError(
53                        "Failed to get target project data".to_string(),
54                    )
55                    .into());
56                }
57                Err(e) => {
58                    return Err(error::gen(format!(
59                        "Could not get target project data for {} with error {}",
60                        args.target_repo, e
61                    )))
62                }
63            }
64        }
65        let url = format!("{}/merge_requests", self.rest_api_basepath());
66        let response = query::send_raw(
67            &self.runner,
68            &url,
69            Some(&body),
70            self.headers(),
71            ApiOperation::MergeRequest,
72            http::Method::POST,
73        )?;
74        // if status code is 409, it means that the merge request already
75        // exists. We already pushed the branch, just return the merge request
76        // as if it was created.
77        if response.status == 409 {
78            // {\"message\":[\"Another open merge request already exists for
79            // this source branch: !60\"]}"
80            let merge_request_json: serde_json::Value = serde_json::from_str(&response.body)?;
81            let merge_request_iid = merge_request_json["message"][0]
82                .as_str()
83                .unwrap()
84                .split_whitespace()
85                .last()
86                .unwrap()
87                .trim_matches('!');
88            if args.amend {
89                let url = format!(
90                    "{}/merge_requests/{}",
91                    self.rest_api_basepath(),
92                    merge_request_iid
93                );
94                query::send_raw(
95                    &self.runner,
96                    &url,
97                    Some(&body),
98                    self.headers(),
99                    ApiOperation::MergeRequest,
100                    http::Method::PUT,
101                )?;
102            }
103            let merge_request_url = format!(
104                "https://{}/{}/-/merge_requests/{}",
105                self.domain, self.path, merge_request_iid
106            );
107            return Ok(MergeRequestResponse::builder()
108                .id(merge_request_iid.parse().unwrap())
109                .web_url(merge_request_url)
110                .build()
111                .unwrap());
112        }
113        if response.status != 201 {
114            return Err(error::gen(format!(
115                "Failed to open merge request: {}",
116                response.body
117            )));
118        }
119        let merge_request_json = json_loads(&response.body)?;
120
121        Ok(MergeRequestResponse::builder()
122            .id(merge_request_json["iid"].as_i64().unwrap())
123            .web_url(merge_request_json["web_url"].as_str().unwrap().to_string())
124            .build()
125            .unwrap())
126    }
127
128    fn list(&self, args: MergeRequestListBodyArgs) -> Result<Vec<MergeRequestResponse>> {
129        let url = self.list_merge_request_url(&args, false);
130        query::paged(
131            &self.runner,
132            &url,
133            args.list_args,
134            self.headers(),
135            None,
136            ApiOperation::MergeRequest,
137            |value| GitlabMergeRequestFields::from(value).into(),
138        )
139    }
140
141    fn merge(&self, id: i64) -> Result<MergeRequestResponse> {
142        // PUT /projects/:id/merge_requests/:merge_request_iid/merge
143        let url = format!("{}/merge_requests/{}/merge", self.rest_api_basepath(), id);
144        query::send::<_, (), _>(
145            &self.runner,
146            &url,
147            None,
148            self.headers(),
149            ApiOperation::MergeRequest,
150            |value| GitlabMergeRequestFields::from(value).into(),
151            http::Method::PUT,
152        )
153    }
154
155    fn get(&self, id: i64) -> Result<MergeRequestResponse> {
156        // GET /projects/:id/merge_requests/:merge_request_iid
157        let url = format!("{}/merge_requests/{}", self.rest_api_basepath(), id);
158        query::get::<_, (), _>(
159            &self.runner,
160            &url,
161            None,
162            self.headers(),
163            ApiOperation::MergeRequest,
164            |value| GitlabMergeRequestFields::from(value).into(),
165        )
166    }
167
168    fn close(&self, id: i64) -> Result<MergeRequestResponse> {
169        let url = format!("{}/merge_requests/{}", self.rest_api_basepath(), id);
170        let mut body = Body::new();
171        body.add("state_event", "close");
172        query::send::<_, &str, _>(
173            &self.runner,
174            &url,
175            Some(&body),
176            self.headers(),
177            ApiOperation::MergeRequest,
178            |value| GitlabMergeRequestFields::from(value).into(),
179            http::Method::PUT,
180        )
181    }
182
183    fn num_pages(&self, args: MergeRequestListBodyArgs) -> Result<Option<u32>> {
184        let url = self.list_merge_request_url(&args, true);
185        let mut headers = Headers::new();
186        headers.set("PRIVATE-TOKEN", self.api_token());
187        query::num_pages(&self.runner, &url, headers, ApiOperation::MergeRequest)
188    }
189
190    fn num_resources(&self, args: MergeRequestListBodyArgs) -> Result<Option<NumberDeltaErr>> {
191        let url = self.list_merge_request_url(&args, true);
192        let mut headers = Headers::new();
193        headers.set("PRIVATE-TOKEN", self.api_token());
194        query::num_resources(&self.runner, &url, headers, ApiOperation::MergeRequest)
195    }
196
197    fn approve(&self, id: i64) -> Result<MergeRequestResponse> {
198        let url = format!("{}/merge_requests/{}/approve", self.rest_api_basepath(), id);
199        let result = query::send::<_, (), MergeRequestResponse>(
200            &self.runner,
201            &url,
202            None,
203            self.headers(),
204            ApiOperation::MergeRequest,
205            |value| GitlabMergeRequestFields::from(value).into(),
206            http::Method::POST,
207        );
208        // responses in approvals for Gitlab do not contain the merge request
209        // URL, patch it in the response.
210        if let Ok(mut response) = result {
211            response.web_url = self.get_url(BrowseOptions::MergeRequestId(id));
212            return Ok(response);
213        }
214        result
215    }
216}
217
218impl<R> Gitlab<R> {
219    fn list_merge_request_url(&self, args: &MergeRequestListBodyArgs, num_pages: bool) -> String {
220        let mut url = if let Some(assignee) = &args.assignee {
221            format!(
222                "{}?state={}&assignee_id={}",
223                self.merge_requests_url, args.state, assignee.id
224            )
225        } else if let Some(reviewer) = &args.reviewer {
226            format!(
227                "{}?state={}&reviewer_id={}",
228                self.merge_requests_url, args.state, reviewer.id
229            )
230        } else if let Some(author) = &args.author {
231            format!(
232                "{}?state={}&author_id={}",
233                self.merge_requests_url, args.state, author.id
234            )
235        } else {
236            format!(
237                "{}/merge_requests?state={}",
238                self.rest_api_basepath(),
239                args.state
240            )
241        };
242        if num_pages {
243            url.push_str("&page=1");
244        }
245        url
246    }
247
248    fn resource_comments_metadata_url(&self, args: CommentMergeRequestListBodyArgs) -> String {
249        let url = format!(
250            "{}/merge_requests/{}/notes?page=1",
251            self.rest_api_basepath(),
252            args.id
253        );
254        url
255    }
256}
257
258impl<R: HttpRunner<Response = HttpResponse>> CommentMergeRequest for Gitlab<R> {
259    fn create(&self, args: CommentMergeRequestBodyArgs) -> Result<()> {
260        let url = format!(
261            "{}/merge_requests/{}/notes",
262            self.rest_api_basepath(),
263            args.id
264        );
265        let mut body = Body::new();
266        body.add("body", args.comment);
267        query::send_raw(
268            &self.runner,
269            &url,
270            Some(&body),
271            self.headers(),
272            ApiOperation::MergeRequest,
273            http::Method::POST,
274        )?;
275        Ok(())
276    }
277
278    fn list(&self, args: CommentMergeRequestListBodyArgs) -> Result<Vec<Comment>> {
279        let url = format!(
280            "{}/merge_requests/{}/notes",
281            self.rest_api_basepath(),
282            args.id
283        );
284
285        query::paged(
286            &self.runner,
287            &url,
288            args.list_args,
289            self.headers(),
290            None,
291            ApiOperation::MergeRequest,
292            |value| GitlabMergeRequestCommentFields::from(value).into(),
293        )
294    }
295
296    fn num_pages(&self, args: CommentMergeRequestListBodyArgs) -> Result<Option<u32>> {
297        let url = self.resource_comments_metadata_url(args);
298        query::num_pages(
299            &self.runner,
300            &url,
301            self.headers(),
302            ApiOperation::MergeRequest,
303        )
304    }
305
306    fn num_resources(
307        &self,
308        args: CommentMergeRequestListBodyArgs,
309    ) -> Result<Option<NumberDeltaErr>> {
310        let url = self.resource_comments_metadata_url(args);
311        query::num_resources(
312            &self.runner,
313            &url,
314            self.headers(),
315            ApiOperation::MergeRequest,
316        )
317    }
318}
319
320pub struct GitlabMergeRequestFields {
321    fields: MergeRequestResponse,
322}
323
324impl From<&serde_json::Value> for GitlabMergeRequestFields {
325    fn from(data: &serde_json::Value) -> Self {
326        GitlabMergeRequestFields {
327            fields: MergeRequestResponse::builder()
328                .id(data["iid"].as_i64().unwrap_or_default())
329                .web_url(data["web_url"].as_str().unwrap_or_default().to_string())
330                .source_branch(
331                    data["source_branch"]
332                        .as_str()
333                        .unwrap_or_default()
334                        .to_string(),
335                )
336                .sha(
337                    data["merge_commit_sha"]
338                        .as_str()
339                        .unwrap_or_default()
340                        .to_string(),
341                )
342                .author(
343                    data["author"]["username"]
344                        .as_str()
345                        .unwrap_or_default()
346                        .to_string(),
347                )
348                .updated_at(data["updated_at"].as_str().unwrap_or_default().to_string())
349                .created_at(data["created_at"].as_str().unwrap_or_default().to_string())
350                .title(data["title"].as_str().unwrap_or_default().to_string())
351                .description(data["description"].as_str().unwrap_or_default().to_string())
352                // If merge request is not merged, merged_at is an empty string.
353                .merged_at(data["merged_at"].as_str().unwrap_or_default().to_string())
354                // Documentation recommends gathering head_pipeline instead of
355                // pipeline key.
356                .pipeline_id(data["head_pipeline"]["id"].as_i64())
357                .pipeline_url(
358                    data["head_pipeline"]["web_url"]
359                        .as_str()
360                        .map(|s| s.to_string()),
361                )
362                .build()
363                .unwrap(),
364        }
365    }
366}
367
368impl From<GitlabMergeRequestFields> for MergeRequestResponse {
369    fn from(fields: GitlabMergeRequestFields) -> Self {
370        fields.fields
371    }
372}
373
374pub struct GitlabMergeRequestCommentFields {
375    comment: Comment,
376}
377
378impl From<&serde_json::Value> for GitlabMergeRequestCommentFields {
379    fn from(data: &serde_json::Value) -> Self {
380        GitlabMergeRequestCommentFields {
381            comment: Comment::builder()
382                .id(data["id"].as_i64().unwrap_or_default())
383                .body(data["body"].as_str().unwrap_or_default().to_string())
384                .author(
385                    data["author"]["username"]
386                        .as_str()
387                        .unwrap_or_default()
388                        .to_string(),
389                )
390                .created_at(data["created_at"].as_str().unwrap_or_default().to_string())
391                .build()
392                .unwrap(),
393        }
394    }
395}
396
397impl From<GitlabMergeRequestCommentFields> for Comment {
398    fn from(fields: GitlabMergeRequestCommentFields) -> Self {
399        fields.comment
400    }
401}
402
403#[cfg(test)]
404mod test {
405
406    use crate::cmds::merge_request::MergeRequestState;
407    use crate::cmds::project::Member;
408    use crate::remote::ListBodyArgs;
409    use crate::setup_client;
410    use crate::test::utils::{
411        default_gitlab, get_contract, BasePath, ClientType, ContractType, Domain, ResponseContracts,
412    };
413
414    use super::*;
415
416    #[test]
417    fn test_list_merge_request_with_from_page() {
418        let contracts =
419            ResponseContracts::new(ContractType::Gitlab).add_body(200, Some("[]"), None);
420        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
421        let args = MergeRequestListBodyArgs::builder()
422            .state(MergeRequestState::Opened)
423            .list_args(Some(
424                ListBodyArgs::builder()
425                    .page(2)
426                    .max_pages(2)
427                    .build()
428                    .unwrap(),
429            ))
430            .assignee(None)
431            .build()
432            .unwrap();
433        gitlab.list(args).unwrap();
434        assert_eq!(
435            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests?state=opened&page=2",
436            *client.url(),
437        );
438    }
439
440    #[test]
441    fn test_list_all_merge_requests_assigned_for_current_user() {
442        let contract = ResponseContracts::new(ContractType::Gitlab).add_body(200, Some("[]"), None);
443        let (client, gitlab) = setup_client!(contract, default_gitlab(), dyn MergeRequest);
444        let args = MergeRequestListBodyArgs::builder()
445            .state(MergeRequestState::Opened)
446            .list_args(None)
447            .assignee(Some(
448                Member::builder()
449                    .name("tom".to_string())
450                    .username("tsawyer".to_string())
451                    .id(1234)
452                    .build()
453                    .unwrap(),
454            ))
455            .build()
456            .unwrap();
457        gitlab.list(args).unwrap();
458        assert_eq!(
459            "https://gitlab.com/api/v4/merge_requests?state=opened&assignee_id=1234",
460            *client.url(),
461        );
462    }
463
464    #[test]
465    fn test_list_all_merge_requests_auth_user_is_reviewer() {
466        let contract = ResponseContracts::new(ContractType::Gitlab).add_body(200, Some("[]"), None);
467        let (client, gitlab) = setup_client!(contract, default_gitlab(), dyn MergeRequest);
468        let args = MergeRequestListBodyArgs::builder()
469            .state(MergeRequestState::Opened)
470            .list_args(None)
471            .reviewer(Some(
472                Member::builder()
473                    .name("tom".to_string())
474                    .username("tsawyer".to_string())
475                    .id(123)
476                    .build()
477                    .unwrap(),
478            ))
479            .build()
480            .unwrap();
481        gitlab.list(args).unwrap();
482        assert_eq!(
483            "https://gitlab.com/api/v4/merge_requests?state=opened&reviewer_id=123",
484            *client.url(),
485        );
486    }
487
488    #[test]
489    fn test_list_all_merge_requests_auth_user_is_the_author() {
490        let contract = ResponseContracts::new(ContractType::Gitlab).add_body(200, Some("[]"), None);
491        let (client, gitlab) = setup_client!(contract, default_gitlab(), dyn MergeRequest);
492        let args = MergeRequestListBodyArgs::builder()
493            .state(MergeRequestState::Opened)
494            .list_args(None)
495            .author(Some(
496                Member::builder()
497                    .name("tom".to_string())
498                    .username("tsawyer".to_string())
499                    .id(192)
500                    .build()
501                    .unwrap(),
502            ))
503            .build()
504            .unwrap();
505        gitlab.list(args).unwrap();
506        assert_eq!(
507            "https://gitlab.com/api/v4/merge_requests?state=opened&author_id=192",
508            *client.url(),
509        );
510    }
511
512    #[test]
513    fn test_open_merge_request() {
514        let assignee = Member::builder()
515            .name("tom".to_string())
516            .username("tsawyer".to_string())
517            .mr_member_type(MrMemberType::Filled)
518            .id(1234)
519            .build()
520            .unwrap();
521        let reviewer = Member::builder()
522            .name("huck".to_string())
523            .username("hfinn".to_string())
524            .mr_member_type(MrMemberType::Filled)
525            .id(5678)
526            .build()
527            .unwrap();
528        let mr_args = MergeRequestBodyArgs::builder()
529            .assignee(assignee)
530            .reviewer(reviewer)
531            .build()
532            .unwrap();
533        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
534            201,
535            "merge_request.json",
536            None,
537        );
538        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
539        assert!(gitlab.open(mr_args).is_ok());
540        assert_eq!(
541            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests",
542            *client.url(),
543        );
544        let mut actual_method = client.http_method.borrow_mut();
545        assert_eq!(http::Method::POST, actual_method.pop().unwrap());
546        assert_eq!(
547            Some(ApiOperation::MergeRequest),
548            *client.api_operation.borrow()
549        );
550        let actual_body = client.request_body.borrow();
551        assert!(actual_body.contains("assignee_id"));
552        assert!(actual_body.contains("reviewer_ids"));
553    }
554
555    #[test]
556    fn test_open_merge_request_with_no_assignee() {
557        let assignee = Member::default();
558        let mr_args = MergeRequestBodyArgs::builder()
559            .assignee(assignee)
560            .build()
561            .unwrap();
562        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
563            201,
564            "merge_request.json",
565            None,
566        );
567        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
568        assert!(gitlab.open(mr_args).is_ok());
569        assert_eq!(
570            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests",
571            *client.url(),
572        );
573        let mut actual_method = client.http_method.borrow_mut();
574        assert_eq!(http::Method::POST, actual_method.pop().unwrap());
575        assert_eq!(
576            Some(ApiOperation::MergeRequest),
577            *client.api_operation.borrow()
578        );
579        let actual_body = client.request_body.borrow();
580        assert!(!actual_body.contains("assignee_id"));
581    }
582
583    #[test]
584    fn test_open_merge_request_target_repo() {
585        // current repo, targeting jordilin/gitar
586        let client_type = ClientType::Gitlab(
587            Domain("gitlab.com".to_string()),
588            BasePath("jdoe/gitar".to_string()),
589        );
590        let responses = ResponseContracts::new(ContractType::Gitlab)
591            .add_contract(201, "merge_request.json", None)
592            .add_contract(200, "project.json", None);
593        let (client, gitlab) = setup_client!(responses, client_type, dyn MergeRequest);
594        let mr_args = MergeRequestBodyArgs::builder()
595            .target_repo("jordilin/gitar".to_string())
596            .build()
597            .unwrap();
598        assert!(gitlab.open(mr_args).is_ok());
599        assert_eq!(
600            "https://gitlab.com/api/v4/projects/jdoe%2Fgitar/merge_requests",
601            *client.url(),
602        );
603        assert_eq!(
604            Some(ApiOperation::MergeRequest),
605            *client.api_operation.borrow()
606        );
607    }
608
609    #[test]
610    fn test_open_merge_request_error() {
611        let contracts =
612            ResponseContracts::new(ContractType::Gitlab).add_body::<String>(400, None, None);
613        let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
614        let mr_args = MergeRequestBodyArgs::builder().build().unwrap();
615        assert!(gitlab.open(mr_args).is_err());
616    }
617
618    #[test]
619    fn test_merge_request_already_exists_status_code_409_conflict() {
620        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
621            409,
622            "merge_request_conflict.json",
623            None,
624        );
625        let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
626        let mr_args = MergeRequestBodyArgs::builder().build().unwrap();
627        assert!(gitlab.open(mr_args).is_ok());
628    }
629
630    #[test]
631    fn test_amend_existing_merge_request() {
632        let contracts = ResponseContracts::new(ContractType::Gitlab)
633            .add_contract(200, "merge_request.json", None)
634            .add_contract(409, "merge_request_conflict.json", None);
635        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
636        let mr_args = MergeRequestBodyArgs::builder().amend(true).build().unwrap();
637        assert!(gitlab.open(mr_args).is_ok());
638        assert_eq!(
639            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests/33",
640            *client.url()
641        );
642        let actual_method = client.http_method.borrow();
643        assert_eq!(http::Method::PUT, actual_method[1]);
644    }
645
646    #[test]
647    fn test_gitlab_merge_request_num_pages() {
648        let link_header = "<https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests?state=opened&page=1>; rel=\"next\", <https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests?state=opened&page=2>; rel=\"last\"";
649        let mut headers = Headers::new();
650        headers.set("link", link_header);
651        let contracts = ResponseContracts::new(ContractType::Gitlab).add_body::<String>(
652            200,
653            None,
654            Some(headers),
655        );
656        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
657        let body_args = MergeRequestListBodyArgs::builder()
658            .state(MergeRequestState::Opened)
659            .list_args(None)
660            .assignee(None)
661            .build()
662            .unwrap();
663        assert_eq!(Some(2), gitlab.num_pages(body_args).unwrap());
664        assert_eq!(
665            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests?state=opened&page=1",
666            *client.url(),
667        );
668    }
669
670    #[test]
671    fn test_gitlab_merge_request_num_pages_current_auth_user() {
672        let link_header = "<https://gitlab.com/api/v4/merge_requests?state=opened&assignee_id=1234&page=1>; rel=\"next\", <https://gitlab.com/api/v4/merge_requests?state=opened&assignee_id=1234&page=2>; rel=\"last\"";
673        let mut headers = Headers::new();
674        headers.set("link", link_header);
675        let contracts = ResponseContracts::new(ContractType::Gitlab).add_body::<String>(
676            200,
677            None,
678            Some(headers),
679        );
680        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
681        let body_args = MergeRequestListBodyArgs::builder()
682            .state(MergeRequestState::Opened)
683            .list_args(None)
684            .assignee(Some(
685                Member::builder()
686                    .name("tom".to_string())
687                    .username("tsawyer".to_string())
688                    .id(1234)
689                    .build()
690                    .unwrap(),
691            ))
692            .build()
693            .unwrap();
694        assert_eq!(Some(2), gitlab.num_pages(body_args).unwrap());
695        assert_eq!(
696            "https://gitlab.com/api/v4/merge_requests?state=opened&assignee_id=1234&page=1",
697            *client.url(),
698        );
699    }
700
701    #[test]
702    fn test_gitlab_merge_request_num_pages_no_link_header_error() {
703        let contracts =
704            ResponseContracts::new(ContractType::Gitlab).add_body::<String>(200, None, None);
705        let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
706        let body_args = MergeRequestListBodyArgs::builder()
707            .state(MergeRequestState::Opened)
708            .list_args(None)
709            .assignee(None)
710            .build()
711            .unwrap();
712        assert_eq!(Some(1), gitlab.num_pages(body_args).unwrap());
713    }
714
715    #[test]
716    fn test_gitlab_merge_request_num_pages_response_error_is_error() {
717        let contracts =
718            ResponseContracts::new(ContractType::Gitlab).add_body::<String>(400, None, None);
719        let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
720        let body_args = MergeRequestListBodyArgs::builder()
721            .state(MergeRequestState::Opened)
722            .list_args(None)
723            .assignee(None)
724            .build()
725            .unwrap();
726        assert!(gitlab.num_pages(body_args).is_err());
727    }
728
729    #[test]
730    fn test_gitlab_merge_request_num_pages_no_last_header_in_link() {
731        let link_header = "<https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests?state=opened&page=1>; rel=\"next\"";
732        let mut headers = Headers::new();
733        headers.set("link", link_header);
734        let contracts = ResponseContracts::new(ContractType::Gitlab).add_body::<String>(
735            200,
736            None,
737            Some(headers),
738        );
739        let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
740        let body_args = MergeRequestListBodyArgs::builder()
741            .state(MergeRequestState::Opened)
742            .list_args(None)
743            .assignee(None)
744            .build()
745            .unwrap();
746        assert_eq!(None, gitlab.num_pages(body_args).unwrap());
747    }
748
749    #[test]
750    fn test_gitlab_create_merge_request_comment_ok() {
751        let contracts =
752            ResponseContracts::new(ContractType::Gitlab).add_body::<String>(201, None, None);
753        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CommentMergeRequest);
754        let comment_args = CommentMergeRequestBodyArgs::builder()
755            .id(1456)
756            .comment("LGTM, ship it".to_string())
757            .build()
758            .unwrap();
759        gitlab.create(comment_args).unwrap();
760        assert_eq!(
761            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests/1456/notes",
762            *client.url()
763        );
764        assert_eq!(
765            Some(ApiOperation::MergeRequest),
766            *client.api_operation.borrow()
767        );
768    }
769
770    #[test]
771    fn test_gitlab_create_merge_request_comment_error() {
772        let contracts =
773            ResponseContracts::new(ContractType::Gitlab).add_body::<String>(400, None, None);
774        let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn CommentMergeRequest);
775        let comment_args = CommentMergeRequestBodyArgs::builder()
776            .id(1456)
777            .comment("LGTM, ship it".to_string())
778            .build()
779            .unwrap();
780        assert!(gitlab.create(comment_args).is_err());
781    }
782
783    #[test]
784    fn test_get_gitlab_merge_request_details() {
785        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
786            200,
787            "merge_request.json",
788            None,
789        );
790        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
791        let merge_request_id = 123456;
792        gitlab.get(merge_request_id).unwrap();
793        assert_eq!(
794            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests/123456",
795            *client.url()
796        );
797        assert_eq!(
798            Some(ApiOperation::MergeRequest),
799            *client.api_operation.borrow()
800        );
801    }
802
803    #[test]
804    fn test_merge_merge_request() {
805        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
806            200,
807            "merge_request.json",
808            None,
809        );
810        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
811        let merge_request_id = 33;
812        gitlab.merge(merge_request_id).unwrap();
813        assert_eq!(
814            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests/33/merge",
815            *client.url()
816        );
817        assert_eq!(
818            Some(ApiOperation::MergeRequest),
819            *client.api_operation.borrow()
820        );
821    }
822
823    #[test]
824    fn test_close_merge_request() {
825        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
826            200,
827            "merge_request.json",
828            None,
829        );
830        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
831        let merge_request_id = 33;
832        gitlab.close(merge_request_id).unwrap();
833        assert_eq!(
834            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests/33",
835            *client.url()
836        );
837        let mut actual_method = client.http_method.borrow_mut();
838        assert_eq!(http::Method::PUT, actual_method.pop().unwrap());
839        assert_eq!(
840            Some(ApiOperation::MergeRequest),
841            *client.api_operation.borrow()
842        );
843    }
844
845    #[test]
846    fn test_approve_merge_request_ok() {
847        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
848            200,
849            "approve_merge_request.json",
850            None,
851        );
852        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn MergeRequest);
853        let merge_request_id = 33;
854        let result = gitlab.approve(merge_request_id);
855        match result {
856            Ok(response) => {
857                assert_eq!(
858                    "https://gitlab.com/jordilin/gitlapi/-/merge_requests/33",
859                    response.web_url
860                );
861            }
862            Err(e) => {
863                panic!(
864                    "Expected Ok merge request approval but got: {:?} instead",
865                    e
866                );
867            }
868        }
869        assert_eq!(
870            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests/33/approve",
871            *client.url()
872        );
873        let mut actual_method = client.http_method.borrow_mut();
874        assert_eq!(http::Method::POST, actual_method.pop().unwrap());
875        assert_eq!(
876            Some(ApiOperation::MergeRequest),
877            *client.api_operation.borrow()
878        );
879    }
880
881    #[test]
882    fn test_list_merge_request_comments() {
883        let contracts = ResponseContracts::new(ContractType::Gitlab).add_body(
884            200,
885            Some(format!(
886                "[{}]",
887                get_contract(ContractType::Gitlab, "comment.json")
888            )),
889            None,
890        );
891        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CommentMergeRequest);
892        let args = CommentMergeRequestListBodyArgs::builder()
893            .id(123)
894            .list_args(None)
895            .build()
896            .unwrap();
897        let comments = gitlab.list(args).unwrap();
898        assert_eq!(1, comments.len());
899        assert_eq!(
900            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests/123/notes",
901            *client.url()
902        );
903        assert_eq!(
904            Some(ApiOperation::MergeRequest),
905            *client.api_operation.borrow()
906        );
907    }
908
909    #[test]
910    fn test_merge_request_comments_num_pages() {
911        let link_header = "<https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests/123/notes?page=1>; rel=\"next\", <https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests/123/notes?page=2>; rel=\"last\"";
912        let mut headers = Headers::new();
913        headers.set("link", link_header);
914        let contracts = ResponseContracts::new(ContractType::Gitlab).add_body::<String>(
915            200,
916            None,
917            Some(headers),
918        );
919        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CommentMergeRequest);
920        let args = CommentMergeRequestListBodyArgs::builder()
921            .id(123)
922            .list_args(None)
923            .build()
924            .unwrap();
925        assert_eq!(Some(2), gitlab.num_pages(args).unwrap());
926        assert_eq!(
927            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/merge_requests/123/notes?page=1",
928            *client.url(),
929        );
930        assert_eq!(
931            Some(ApiOperation::MergeRequest),
932            *client.api_operation.borrow()
933        );
934    }
935}