gr/gitlab/
cicd.rs

1use super::Gitlab;
2use crate::api_traits::{ApiOperation, CicdJob, CicdRunner};
3use crate::cmds::cicd::{
4    Job, JobListBodyArgs, LintResponse, Pipeline, PipelineBodyArgs, Runner, RunnerListBodyArgs,
5    RunnerMetadata, RunnerPostDataCliArgs, RunnerRegistrationResponse, RunnerStatus, YamlBytes,
6};
7use crate::http::{self, Body, Headers};
8use crate::remote::{query, URLQueryParamBuilder};
9use crate::{
10    api_traits::Cicd,
11    io::{HttpResponse, HttpRunner},
12};
13use crate::{time, Result};
14
15impl<R: HttpRunner<Response = HttpResponse>> Cicd for Gitlab<R> {
16    fn list(&self, args: PipelineBodyArgs) -> Result<Vec<Pipeline>> {
17        let url = format!("{}/pipelines", self.rest_api_basepath());
18        query::paged(
19            &self.runner,
20            &url,
21            args.from_to_page,
22            self.headers(),
23            None,
24            ApiOperation::Pipeline,
25            |value| GitlabPipelineFields::from(value).into(),
26        )
27    }
28
29    fn get_pipeline(&self, _id: i64) -> Result<Pipeline> {
30        todo!();
31    }
32
33    fn num_pages(&self) -> Result<Option<u32>> {
34        let (url, headers) = self.resource_cicd_metadata_url();
35        query::num_pages(&self.runner, &url, headers, ApiOperation::Pipeline)
36    }
37
38    fn num_resources(&self) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
39        let (url, headers) = self.resource_cicd_metadata_url();
40        query::num_resources(&self.runner, &url, headers, ApiOperation::Pipeline)
41    }
42
43    // https://docs.gitlab.com/ee/api/lint.html#validate-the-ci-yaml-configuration
44    fn lint(&self, body: YamlBytes) -> Result<LintResponse> {
45        let url = format!("{}/ci/lint", self.rest_api_basepath());
46        let mut payload = Body::new();
47        payload.add("content", body.to_string());
48        query::send(
49            &self.runner,
50            &url,
51            Some(&payload),
52            self.headers(),
53            ApiOperation::Pipeline,
54            |value| GitlabLintResponseFields::from(value).into(),
55            http::Method::POST,
56        )
57    }
58}
59
60impl<R: HttpRunner<Response = HttpResponse>> CicdRunner for Gitlab<R> {
61    fn list(&self, args: RunnerListBodyArgs) -> Result<Vec<crate::cmds::cicd::Runner>> {
62        let url = self.list_runners_url(&args, false);
63        query::paged(
64            &self.runner,
65            &url,
66            args.list_args,
67            self.headers(),
68            None,
69            ApiOperation::Pipeline,
70            |value| GitlabRunnerFields::from(value).into(),
71        )
72    }
73
74    fn get(&self, id: i64) -> Result<RunnerMetadata> {
75        let url = format!("{}/{}", self.base_runner_url, id);
76        query::get::<_, (), _>(
77            &self.runner,
78            &url,
79            None,
80            self.headers(),
81            ApiOperation::Pipeline,
82            |value| GitlabRunnerMetadataFields::from(value).into(),
83        )
84    }
85
86    fn num_pages(&self, args: RunnerListBodyArgs) -> Result<Option<u32>> {
87        let url = self.list_runners_url(&args, true);
88        query::num_pages(&self.runner, &url, self.headers(), ApiOperation::Pipeline)
89    }
90
91    fn num_resources(
92        &self,
93        args: RunnerListBodyArgs,
94    ) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
95        let url = self.list_runners_url(&args, true);
96        query::num_resources(&self.runner, &url, self.headers(), ApiOperation::Pipeline)
97    }
98
99    /// Creates a new runner based in the authentication token workflow as
100    /// opposed to the registration based workflow which gets deprecated in
101    /// Gitlab > 16.0. The response includes an auth token that can be included
102    /// in the runner's configuration file.
103    /// API doc https://docs.gitlab.com/ee/api/users.html#create-a-runner-linked-to-a-user
104    fn create(&self, args: RunnerPostDataCliArgs) -> Result<RunnerRegistrationResponse> {
105        let url = format!("{}/runners", self.base_current_user_url);
106        let mut body = Body::new();
107        if args.description.is_some() {
108            body.add("description", args.description.unwrap());
109        }
110        // Run untagged is the default (optional), so if no run_untagged field
111        // is set in the HTTP body, it is understood runner can run untagged
112        // jobs. If user does not provide the --run-untagged, then we need to
113        // set it to false.
114        if !args.run_untagged {
115            body.add("run_untagged", "false".to_string());
116        }
117        if args.tags.is_some() {
118            body.add("tag_list", args.tags.unwrap());
119        }
120        if args.project_id.is_some() {
121            body.add("project_id", args.project_id.unwrap().to_string());
122        }
123        if args.group_id.is_some() {
124            body.add("group_id", args.group_id.unwrap().to_string());
125        }
126        body.add("runner_type", args.kind.to_string());
127
128        query::send(
129            &self.runner,
130            &url,
131            Some(&body),
132            self.headers(),
133            ApiOperation::Pipeline,
134            |value| GitlabCreateRunnerFields::from(value).into(),
135            http::Method::POST,
136        )
137    }
138}
139
140pub struct GitlabCreateRunnerFields {
141    field: RunnerRegistrationResponse,
142}
143
144impl From<&serde_json::Value> for GitlabCreateRunnerFields {
145    fn from(data: &serde_json::Value) -> Self {
146        GitlabCreateRunnerFields {
147            field: RunnerRegistrationResponse::builder()
148                .id(data["id"].as_i64().unwrap_or_default())
149                .token(data["token"].as_str().unwrap_or_default().to_string())
150                .token_expiration(
151                    data["token_expiration"]
152                        .as_str()
153                        .unwrap_or_default()
154                        .to_string(),
155                )
156                .build()
157                .unwrap(),
158        }
159    }
160}
161
162impl From<GitlabCreateRunnerFields> for RunnerRegistrationResponse {
163    fn from(fields: GitlabCreateRunnerFields) -> Self {
164        fields.field
165    }
166}
167
168pub struct GitlabCicdJobFields {
169    job: Job,
170}
171
172impl From<&serde_json::Value> for GitlabCicdJobFields {
173    fn from(data: &serde_json::Value) -> Self {
174        GitlabCicdJobFields {
175            job: Job::builder()
176                .id(data["id"].as_i64().unwrap_or_default())
177                .name(data["name"].as_str().unwrap_or_default().to_string())
178                .branch(data["ref"].as_str().unwrap_or_default().to_string())
179                .url(data["web_url"].as_str().unwrap_or_default().to_string())
180                .author_name(
181                    data["user"]["name"]
182                        .as_str()
183                        .unwrap_or_default()
184                        .to_string(),
185                )
186                .commit_sha(
187                    data["commit"]["id"]
188                        .as_str()
189                        .unwrap_or_default()
190                        .to_string(),
191                )
192                .pipeline_id(data["pipeline"]["id"].as_i64().unwrap_or_default())
193                .runner_tags(
194                    data["tag_list"]
195                        .as_array()
196                        .unwrap()
197                        .iter()
198                        .map(|v| v.as_str().unwrap().to_string())
199                        .collect(),
200                )
201                .stage(data["stage"].as_str().unwrap_or_default().to_string())
202                .status(data["status"].as_str().unwrap_or_default().to_string())
203                .created_at(data["created_at"].as_str().unwrap_or_default().to_string())
204                .started_at(data["started_at"].as_str().unwrap_or_default().to_string())
205                .finished_at(data["finished_at"].as_str().unwrap_or_default().to_string())
206                .duration(data["duration"].as_f64().unwrap_or_default().to_string())
207                .build()
208                .unwrap(),
209        }
210    }
211}
212
213impl From<GitlabCicdJobFields> for Job {
214    fn from(fields: GitlabCicdJobFields) -> Self {
215        fields.job
216    }
217}
218
219impl<R: HttpRunner<Response = HttpResponse>> CicdJob for Gitlab<R> {
220    // https://docs.gitlab.com/ee/api/jobs.html#list-project-jobs
221    fn list(&self, args: JobListBodyArgs) -> Result<Vec<Job>> {
222        let url = format!("{}/jobs", self.rest_api_basepath());
223        query::paged(
224            &self.runner,
225            &url,
226            args.list_args,
227            self.headers(),
228            None,
229            ApiOperation::Pipeline,
230            |value| GitlabCicdJobFields::from(value).into(),
231        )
232    }
233
234    fn num_pages(&self, _args: JobListBodyArgs) -> Result<Option<u32>> {
235        let url = format!("{}/jobs?page=1", self.rest_api_basepath());
236        query::num_pages(&self.runner, &url, self.headers(), ApiOperation::Pipeline)
237    }
238
239    fn num_resources(
240        &self,
241        _args: JobListBodyArgs,
242    ) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
243        let url = format!("{}/jobs?page=1", self.rest_api_basepath());
244        query::num_resources(&self.runner, &url, self.headers(), ApiOperation::Pipeline)
245    }
246}
247
248impl<R> Gitlab<R> {
249    fn list_runners_url(&self, args: &RunnerListBodyArgs, num_pages: bool) -> String {
250        let base_url = if args.all {
251            format!("{}/all", self.base_runner_url)
252        } else {
253            format!("{}/runners", self.rest_api_basepath(),)
254        };
255        let mut url = URLQueryParamBuilder::new(&base_url);
256        match args.status {
257            RunnerStatus::All => {}
258            _ => {
259                url.add_param("status", &args.status.to_string());
260            }
261        }
262        if num_pages {
263            url.add_param("page", "1");
264        }
265        if let Some(tags) = &args.tags {
266            url.add_param("tag_list", tags);
267        }
268        url.build()
269    }
270
271    fn resource_cicd_metadata_url(&self) -> (String, Headers) {
272        let url = format!("{}/pipelines?page=1", self.rest_api_basepath());
273        let mut headers = Headers::new();
274        headers.set("PRIVATE-TOKEN", self.api_token());
275        (url, headers)
276    }
277}
278
279pub struct GitlabRunnerFields {
280    id: i64,
281    description: String,
282    ip_address: String,
283    active: bool,
284    paused: bool,
285    is_shared: bool,
286    runner_type: String,
287    name: String,
288    online: bool,
289    status: String,
290}
291
292impl From<&serde_json::Value> for GitlabRunnerFields {
293    fn from(value: &serde_json::Value) -> Self {
294        Self {
295            id: value["id"].as_i64().unwrap(),
296            description: value["description"]
297                .as_str()
298                .unwrap_or_default()
299                .to_string(),
300            ip_address: value["ip_address"].as_str().unwrap_or_default().to_string(),
301            active: value["active"].as_bool().unwrap_or_default(),
302            paused: value["paused"].as_bool().unwrap_or_default(),
303            is_shared: value["is_shared"].as_bool().unwrap_or_default(),
304            runner_type: value["runner_type"]
305                .as_str()
306                .unwrap_or_default()
307                .to_string(),
308            name: value["name"].as_str().unwrap_or_default().to_string(),
309            online: value["online"].as_bool().unwrap_or_default(),
310            status: value["status"].as_str().unwrap_or_default().to_string(),
311        }
312    }
313}
314
315impl From<GitlabRunnerFields> for Runner {
316    fn from(fields: GitlabRunnerFields) -> Self {
317        Runner::builder()
318            .id(fields.id)
319            .description(fields.description)
320            .ip_address(fields.ip_address)
321            .active(fields.active)
322            .paused(fields.paused)
323            .is_shared(fields.is_shared)
324            .runner_type(fields.runner_type)
325            .name(fields.name)
326            .online(fields.online)
327            .status(fields.status)
328            .build()
329            .unwrap()
330    }
331}
332
333pub struct GitlabRunnerMetadataFields {
334    pub id: i64,
335    pub run_untagged: bool,
336    pub tag_list: Vec<String>,
337    pub version: String,
338    pub architecture: String,
339    pub platform: String,
340    pub contacted_at: String,
341    pub revision: String,
342}
343
344impl From<&serde_json::Value> for GitlabRunnerMetadataFields {
345    fn from(value: &serde_json::Value) -> Self {
346        Self {
347            id: value["id"].as_i64().unwrap(),
348            run_untagged: value["run_untagged"].as_bool().unwrap(),
349            tag_list: value["tag_list"]
350                .as_array()
351                .unwrap()
352                .iter()
353                .map(|v| v.as_str().unwrap().to_string())
354                .collect(),
355            version: value["version"].as_str().unwrap().to_string(),
356            architecture: value["architecture"].as_str().unwrap().to_string(),
357            platform: value["platform"].as_str().unwrap().to_string(),
358            contacted_at: value["contacted_at"].as_str().unwrap().to_string(),
359            revision: value["revision"].as_str().unwrap().to_string(),
360        }
361    }
362}
363
364impl From<GitlabRunnerMetadataFields> for RunnerMetadata {
365    fn from(fields: GitlabRunnerMetadataFields) -> Self {
366        RunnerMetadata::builder()
367            .id(fields.id)
368            .run_untagged(fields.run_untagged)
369            .tag_list(fields.tag_list)
370            .version(fields.version)
371            .architecture(fields.architecture)
372            .platform(fields.platform)
373            .contacted_at(fields.contacted_at)
374            .revision(fields.revision)
375            .build()
376            .unwrap()
377    }
378}
379
380pub struct GitlabPipelineFields {
381    pipeline: Pipeline,
382}
383
384impl From<&serde_json::Value> for GitlabPipelineFields {
385    fn from(data: &serde_json::Value) -> Self {
386        GitlabPipelineFields {
387            pipeline: Pipeline::builder()
388                .id(data["id"].as_i64().unwrap_or_default())
389                .status(data["status"].as_str().unwrap().to_string())
390                .web_url(data["web_url"].as_str().unwrap().to_string())
391                .branch(data["ref"].as_str().unwrap().to_string())
392                .sha(data["sha"].as_str().unwrap().to_string())
393                .created_at(data["created_at"].as_str().unwrap().to_string())
394                .updated_at(data["updated_at"].as_str().unwrap().to_string())
395                .duration(time::compute_duration(
396                    data["created_at"].as_str().unwrap(),
397                    data["updated_at"].as_str().unwrap(),
398                ))
399                .build()
400                .unwrap(),
401        }
402    }
403}
404
405impl From<GitlabPipelineFields> for Pipeline {
406    fn from(fields: GitlabPipelineFields) -> Self {
407        fields.pipeline
408    }
409}
410
411pub struct GitlabLintResponseFields {
412    lint_response: LintResponse,
413}
414
415impl From<&serde_json::Value> for GitlabLintResponseFields {
416    fn from(data: &serde_json::Value) -> Self {
417        GitlabLintResponseFields {
418            lint_response: LintResponse::builder()
419                .valid(data["valid"].as_bool().unwrap())
420                .errors(
421                    data["errors"]
422                        .as_array()
423                        .unwrap()
424                        .iter()
425                        .map(|v| v.as_str().unwrap().to_string())
426                        .collect(),
427                )
428                .merged_yaml(data["merged_yaml"].as_str().unwrap().to_string())
429                .build()
430                .unwrap(),
431        }
432    }
433}
434
435impl From<GitlabLintResponseFields> for LintResponse {
436    fn from(fields: GitlabLintResponseFields) -> Self {
437        fields.lint_response
438    }
439}
440
441#[cfg(test)]
442mod test {
443
444    use crate::cmds::cicd::{RunnerStatus, RunnerType};
445    use crate::remote::ListBodyArgs;
446    use crate::setup_client;
447    use crate::test::utils::{default_gitlab, ContractType, ResponseContracts};
448
449    use super::*;
450
451    #[test]
452    fn test_list_pipelines_ok() {
453        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
454            200,
455            "list_pipelines.json",
456            None,
457        );
458        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn Cicd);
459        let pipelines = gitlab.list(default_pipeline_body_args()).unwrap();
460
461        assert_eq!(3, pipelines.len());
462        assert_eq!(
463            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/pipelines",
464            *client.url(),
465        );
466        assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
467        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
468    }
469
470    #[test]
471    fn test_list_pipelines_with_stream_ok() {
472        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
473            200,
474            "list_pipelines.json",
475            None,
476        );
477        let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn Cicd);
478        let pipelines = gitlab
479            .list(
480                PipelineBodyArgs::builder()
481                    .from_to_page(Some(ListBodyArgs::builder().flush(true).build().unwrap()))
482                    .build()
483                    .unwrap(),
484            )
485            .unwrap();
486        // pipelines is empty because we are flushing the output to STDOUT on
487        // each request
488        assert_eq!(0, pipelines.len());
489    }
490
491    fn default_pipeline_body_args() -> PipelineBodyArgs {
492        let body_args = PipelineBodyArgs::builder()
493            .from_to_page(None)
494            .build()
495            .unwrap();
496        body_args
497    }
498
499    #[test]
500    fn test_list_pipelines_error() {
501        let contracts =
502            ResponseContracts::new(ContractType::Gitlab).add_body::<String>(400, None, None);
503        let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn Cicd);
504        assert!(gitlab.list(default_pipeline_body_args()).is_err());
505    }
506
507    #[test]
508    fn test_no_pipelines() {
509        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
510            200,
511            "no_pipelines.json",
512            None,
513        );
514        let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn Cicd);
515        let pipelines = gitlab.list(default_pipeline_body_args()).unwrap();
516        assert_eq!(0, pipelines.len());
517    }
518
519    #[test]
520    fn test_pipeline_page_from_set_in_url() {
521        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
522            200,
523            "list_pipelines.json",
524            None,
525        );
526        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn Cicd);
527        let fromtopage_args = ListBodyArgs::builder()
528            .page(2)
529            .max_pages(2)
530            .build()
531            .unwrap();
532        let body_args = PipelineBodyArgs::builder()
533            .from_to_page(Some(fromtopage_args))
534            .build()
535            .unwrap();
536        gitlab.list(body_args).unwrap();
537        assert_eq!(
538            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/pipelines?page=2",
539            *client.url(),
540        );
541    }
542
543    #[test]
544    fn test_gitlab_implements_num_pages_pipeline_operation() {
545        let link_header = "<https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/pipelines?page=2>; rel=\"next\", <https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/pipelines?page=2>; rel=\"last\"";
546        let mut headers = Headers::new();
547        headers.set("link", link_header);
548        let contracts = ResponseContracts::new(ContractType::Gitlab).add_body::<String>(
549            200,
550            None,
551            Some(headers),
552        );
553        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn Cicd);
554        assert_eq!(Some(2), gitlab.num_pages().unwrap());
555        assert_eq!(
556            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/pipelines?page=1",
557            *client.url(),
558        );
559    }
560
561    #[test]
562    fn test_gitlab_num_pages_pipeline_no_last_header_in_link() {
563        let link_header = "<https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/pipelines?page=2>; rel=\"next\"";
564        let mut headers = Headers::new();
565        headers.set("link", link_header);
566        let contracts = ResponseContracts::new(ContractType::Gitlab).add_body::<String>(
567            200,
568            None,
569            Some(headers),
570        );
571        let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn Cicd);
572        assert_eq!(None, gitlab.num_pages().unwrap());
573    }
574
575    #[test]
576    fn test_gitlab_num_pages_pipeline_operation_response_error_is_error() {
577        let contracts =
578            ResponseContracts::new(ContractType::Gitlab).add_body::<String>(400, None, None);
579        let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn Cicd);
580        assert!(gitlab.num_pages().is_err());
581    }
582
583    #[test]
584    fn test_list_project_runners() {
585        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
586            200,
587            "list_project_runners.json",
588            None,
589        );
590        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
591        let body_args = RunnerListBodyArgs::builder()
592            .status(RunnerStatus::Online)
593            .list_args(None)
594            .build()
595            .unwrap();
596        gitlab.list(body_args).unwrap();
597        assert_eq!(
598            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/runners?status=online",
599            *client.url(),
600        );
601        assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
602        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
603    }
604
605    #[test]
606    fn test_project_runner_num_pages() {
607        let link_header = "<https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/runners?status=online&page=1>; rel=\"first\", <https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/runners?status=online&page=1>; rel=\"last\"";
608        let mut headers = Headers::new();
609        headers.set("link", link_header);
610        let contracts = ResponseContracts::new(ContractType::Gitlab).add_body::<String>(
611            200,
612            None,
613            Some(headers),
614        );
615        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
616        let body_args = RunnerListBodyArgs::builder()
617            .status(RunnerStatus::Online)
618            .list_args(None)
619            .build()
620            .unwrap();
621        let num_pages = gitlab.num_pages(body_args).unwrap();
622        assert_eq!(
623            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/runners?status=online&page=1",
624            *client.url(),
625        );
626        assert_eq!(Some(1), num_pages);
627    }
628
629    #[test]
630    fn test_get_gitlab_runner_metadata() {
631        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
632            200,
633            "get_runner_details.json",
634            None,
635        );
636        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
637        gitlab.get(11573930).unwrap();
638        assert_eq!("https://gitlab.com/api/v4/runners/11573930", *client.url(),);
639        assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
640        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
641    }
642
643    #[test]
644    fn test_list_gitlab_runners_with_a_tag_list() {
645        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
646            200,
647            "list_project_runners.json",
648            None,
649        );
650        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
651        let body_args = RunnerListBodyArgs::builder()
652            .status(RunnerStatus::Online)
653            .list_args(None)
654            .tags(Some("tag1,tag2".to_string()))
655            .build()
656            .unwrap();
657        gitlab.list(body_args).unwrap();
658        assert_eq!(
659            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/runners?status=online&tag_list=tag1,tag2",
660            *client.url(),
661        );
662        assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
663        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
664    }
665
666    #[test]
667    fn test_get_all_gitlab_runners() {
668        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
669            200,
670            // using same contract as listing project's runners. The schema is
671            // the same
672            "list_project_runners.json",
673            None,
674        );
675        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
676        let body_args = RunnerListBodyArgs::builder()
677            .status(RunnerStatus::Online)
678            .list_args(None)
679            .all(true)
680            .build()
681            .unwrap();
682        let runners = gitlab.list(body_args).unwrap();
683        assert_eq!(
684            "https://gitlab.com/api/v4/runners/all?status=online",
685            *client.url(),
686        );
687        assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
688        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
689        assert_eq!(2, runners.len());
690    }
691
692    #[test]
693    fn test_get_all_gitlab_runners_stream_ok() {
694        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
695            200,
696            // using same contract as listing project's runners. The schema is
697            // the same
698            "list_project_runners.json",
699            None,
700        );
701        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
702        let body_args = RunnerListBodyArgs::builder()
703            .status(RunnerStatus::Online)
704            .list_args(Some(ListBodyArgs::builder().flush(true).build().unwrap()))
705            .all(true)
706            .build()
707            .unwrap();
708        let runners = gitlab.list(body_args).unwrap();
709        assert_eq!(
710            "https://gitlab.com/api/v4/runners/all?status=online",
711            *client.url(),
712        );
713        assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
714        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
715        // We are streaming the output to STDOUT so we should not have any runners
716        // on response.
717        assert_eq!(0, runners.len());
718    }
719
720    #[test]
721    fn test_get_project_runners_in_any_status() {
722        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
723            200,
724            "list_project_runners.json",
725            None,
726        );
727        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
728        let body_args = RunnerListBodyArgs::builder()
729            .status(RunnerStatus::All)
730            .list_args(None)
731            .build()
732            .unwrap();
733        gitlab.list(body_args).unwrap();
734        assert_eq!(
735            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/runners",
736            *client.url(),
737        );
738        assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
739        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
740    }
741
742    #[test]
743    fn test_all_runners_at_any_status_with_tags() {
744        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
745            200,
746            "list_project_runners.json",
747            None,
748        );
749        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
750        let body_args = RunnerListBodyArgs::builder()
751            .status(RunnerStatus::All)
752            .list_args(None)
753            .tags(Some("tag1,tag2".to_string()))
754            .all(true)
755            .build()
756            .unwrap();
757        gitlab.list(body_args).unwrap();
758        assert_eq!(
759            "https://gitlab.com/api/v4/runners/all?tag_list=tag1,tag2",
760            *client.url(),
761        );
762        assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
763        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
764    }
765
766    #[test]
767    fn test_all_runners_at_any_status_with_tags_num_pages() {
768        let link_header = "<https://gitlab.com/api/v4/runners/all?tag_list=tag1,tag2&page=1>; rel=\"first\", <https://gitlab.com/api/v4/runners/all?tag_list=tag1,tag2&page=1>; rel=\"last\"";
769        let mut headers = Headers::new();
770        headers.set("link", link_header);
771        let contracts = ResponseContracts::new(ContractType::Gitlab).add_body::<String>(
772            200,
773            None,
774            Some(headers),
775        );
776        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
777        let body_args = RunnerListBodyArgs::builder()
778            .status(RunnerStatus::All)
779            .list_args(None)
780            .tags(Some("tag1,tag2".to_string()))
781            .all(true)
782            .build()
783            .unwrap();
784        let num_pages = gitlab.num_pages(body_args).unwrap();
785        assert_eq!(
786            "https://gitlab.com/api/v4/runners/all?page=1&tag_list=tag1,tag2",
787            *client.url(),
788        );
789        assert_eq!(Some(1), num_pages);
790    }
791
792    fn gen_gitlab_ci_body<'a>() -> YamlBytes<'a> {
793        YamlBytes::new(
794            b"image: alpine\n\
795          stages:\n\
796            - build\n\
797          build:\n\
798            stage: build\n\
799            script:\n\
800              - echo \"Building\"\n",
801        )
802    }
803
804    #[test]
805    fn test_lint_ci_file_ok() {
806        let contracts =
807            ResponseContracts::new(ContractType::Gitlab).add_contract(201, "ci_lint_ok.json", None);
808        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn Cicd);
809        let response = gitlab.lint(gen_gitlab_ci_body()).unwrap();
810        assert!(response.valid);
811        assert_eq!(
812            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/ci/lint",
813            *client.url()
814        );
815    }
816
817    #[test]
818    fn test_lint_ci_file_error() {
819        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
820            201,
821            "ci_lint_error.json",
822            None,
823        );
824        let (_, gitlab) = setup_client!(contracts, default_gitlab(), dyn Cicd);
825        let result = gitlab.lint(gen_gitlab_ci_body());
826        assert!(result.is_ok());
827        let response = result.unwrap();
828        assert!(!response.valid);
829        assert!(!response.errors.is_empty());
830    }
831
832    #[test]
833    fn test_gitlab_project_pipeline_jobs() {
834        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
835            200,
836            "list_project_jobs.json",
837            None,
838        );
839        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdJob);
840        let body_args = JobListBodyArgs::builder().list_args(None).build().unwrap();
841        gitlab.list(body_args).unwrap();
842        assert_eq!(
843            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/jobs",
844            *client.url()
845        );
846        assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
847        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
848    }
849
850    #[test]
851    fn test_gitlab_project_jobs_num_pages() {
852        let link_header = "<https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/jobs?page=2>; rel=\"next\", <https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/jobs?page=2>; rel=\"last\"";
853        let mut headers = Headers::new();
854        headers.set("link", link_header);
855        let contracts = ResponseContracts::new(ContractType::Gitlab).add_body::<String>(
856            200,
857            None,
858            Some(headers),
859        );
860        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdJob);
861        let body_args = JobListBodyArgs::builder().list_args(None).build().unwrap();
862        let num_pages = gitlab.num_pages(body_args).unwrap();
863        assert_eq!(
864            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/jobs?page=1",
865            *client.url(),
866        );
867        assert_eq!(Some(2), num_pages);
868    }
869
870    #[test]
871    fn test_gitlab_project_jobs_num_resources() {
872        let contracts = ResponseContracts::new(ContractType::Gitlab).add_body::<String>(
873            200,
874            None,
875            Some(Headers::new()),
876        );
877        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdJob);
878        let body_args = JobListBodyArgs::builder().list_args(None).build().unwrap();
879        let num_resources = gitlab.num_resources(body_args).unwrap();
880        assert_eq!(
881            "https://gitlab.com/api/v4/projects/jordilin%2Fgitlapi/jobs?page=1",
882            *client.url()
883        );
884        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
885        assert_eq!("(1, 30)", &num_resources.unwrap().to_string());
886    }
887
888    #[test]
889    fn test_gitlab_create_auth_token_based_instance_runner_with_description_and_tags() {
890        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
891            201,
892            "create_auth_runner_response.json",
893            None,
894        );
895        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
896        let args = RunnerPostDataCliArgs::builder()
897            .description(Some("My runner".to_string()))
898            .tags(Some("tag1,tag2".to_string()))
899            .kind(RunnerType::Instance)
900            .build()
901            .unwrap();
902        let response = gitlab.create(args).unwrap();
903        assert_eq!("https://gitlab.com/api/v4/user/runners", *client.url(),);
904        assert_eq!("1234", client.headers().get("PRIVATE-TOKEN").unwrap());
905        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
906        assert_eq!("newtoken", response.token);
907        let body = client.request_body();
908        assert!(body.contains("description"));
909        assert!(body.contains("tag_list"));
910        assert!(body.contains("instance_type"));
911    }
912
913    #[test]
914    fn test_create_new_runner_tag_list_and_untagged() {
915        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
916            201,
917            "create_auth_runner_response.json",
918            None,
919        );
920        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
921        let args = RunnerPostDataCliArgs::builder()
922            .description(Some("My runner".to_string()))
923            .tags(Some("tag1,tag2".to_string()))
924            .kind(RunnerType::Instance)
925            .run_untagged(true)
926            .build()
927            .unwrap();
928        gitlab.create(args).unwrap();
929        let body = client.request_body();
930        assert!(body.contains("tag_list"));
931        // If not set, can run untagged jobs (default optional)
932        assert!(!body.contains("run_untagged"));
933    }
934
935    #[test]
936    fn test_create_new_runner_untagged_only() {
937        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
938            201,
939            "create_auth_runner_response.json",
940            None,
941        );
942        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
943        let args = RunnerPostDataCliArgs::builder()
944            .description(Some("My runner".to_string()))
945            .kind(RunnerType::Instance)
946            .tags(None)
947            .run_untagged(true)
948            .build()
949            .unwrap();
950        gitlab.create(args).unwrap();
951        let body = client.request_body();
952        assert!(!body.contains("tag_list"));
953        // We do not set it in HTTP body, because it is the default
954        assert!(!body.contains("run_untagged"));
955    }
956
957    #[test]
958    fn test_create_new_runner_tag_list_only() {
959        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
960            201,
961            "create_auth_runner_response.json",
962            None,
963        );
964        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
965        let args = RunnerPostDataCliArgs::builder()
966            .description(Some("My runner".to_string()))
967            .tags(Some("tag1,tag2".to_string()))
968            .kind(RunnerType::Instance)
969            .run_untagged(false)
970            .build()
971            .unwrap();
972        gitlab.create(args).unwrap();
973        let body = client.request_body();
974        assert!(body.contains("tag_list"));
975        // Run untagged is set in HTTP Body as false
976        assert!(body.contains("\"run_untagged\":\"false\""));
977    }
978
979    #[test]
980    fn create_project_runner_has_project_id_in_payload() {
981        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
982            201,
983            "create_auth_runner_response.json",
984            None,
985        );
986        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
987        let args = RunnerPostDataCliArgs::builder()
988            .description(Some("My runner".to_string()))
989            .tags(Some("tag1,tag2".to_string()))
990            .project_id(Some(1234))
991            .kind(RunnerType::Project)
992            .build()
993            .unwrap();
994        gitlab.create(args).unwrap();
995        let body = client.request_body();
996        assert!(body.contains("project_id"));
997    }
998
999    #[test]
1000    fn create_group_runner_has_group_id_in_payload() {
1001        let contracts = ResponseContracts::new(ContractType::Gitlab).add_contract(
1002            201,
1003            "create_auth_runner_response.json",
1004            None,
1005        );
1006        let (client, gitlab) = setup_client!(contracts, default_gitlab(), dyn CicdRunner);
1007        let args = RunnerPostDataCliArgs::builder()
1008            .description(Some("My group runner".to_string()))
1009            .tags(Some("tag1,tag2".to_string()))
1010            .group_id(Some(1234))
1011            .kind(RunnerType::Group)
1012            .build()
1013            .unwrap();
1014        gitlab.create(args).unwrap();
1015        let body = client.request_body();
1016        assert!(body.contains("group_id"));
1017    }
1018}