gr/cmds/
cicd.rs

1use mermaid::{generate_mermaid_stages_diagram, YamlParser};
2use yaml::load_yaml;
3
4use crate::api_traits::{Cicd, CicdJob, CicdRunner, Timestamp};
5use crate::cli::cicd::{JobOptions, PipelineOptions, RunnerOptions};
6use crate::config::ConfigProperties;
7use crate::display::{Column, DisplayBody};
8use crate::remote::{CacheType, GetRemoteCliArgs, ListBodyArgs, ListRemoteCliArgs};
9use crate::{display, error, remote, Result};
10use std::fmt::Display;
11use std::io::{Read, Write};
12use std::sync::Arc;
13
14pub mod mermaid;
15pub mod yaml;
16
17use super::common::{
18    self, num_cicd_pages, num_cicd_resources, num_job_pages, num_job_resources, num_runner_pages,
19    num_runner_resources,
20};
21
22#[derive(Builder, Clone, Debug)]
23pub struct Pipeline {
24    id: i64,
25    pub status: String,
26    web_url: String,
27    branch: String,
28    sha: String,
29    created_at: String,
30    updated_at: String,
31    duration: u64,
32}
33
34impl Pipeline {
35    pub fn builder() -> PipelineBuilder {
36        PipelineBuilder::default()
37    }
38}
39
40impl Timestamp for Pipeline {
41    fn created_at(&self) -> String {
42        self.created_at.clone()
43    }
44}
45
46impl From<Pipeline> for DisplayBody {
47    fn from(p: Pipeline) -> DisplayBody {
48        DisplayBody {
49            columns: vec![
50                Column::new("ID", p.id.to_string()),
51                Column::new("URL", p.web_url),
52                Column::new("Branch", p.branch),
53                Column::new("SHA", p.sha),
54                Column::new("Created at", p.created_at),
55                Column::new("Updated at", p.updated_at),
56                Column::new("Duration", p.duration.to_string()),
57                Column::new("Status", p.status),
58            ],
59        }
60    }
61}
62
63#[derive(Builder, Clone)]
64pub struct PipelineBodyArgs {
65    pub from_to_page: Option<ListBodyArgs>,
66}
67
68impl PipelineBodyArgs {
69    pub fn builder() -> PipelineBodyArgsBuilder {
70        PipelineBodyArgsBuilder::default()
71    }
72}
73
74#[derive(Builder, Clone)]
75pub struct LintFilePathArgs {
76    pub path: String,
77}
78
79impl LintFilePathArgs {
80    pub fn builder() -> LintFilePathArgsBuilder {
81        LintFilePathArgsBuilder::default()
82    }
83}
84
85#[derive(Builder, Clone)]
86pub struct LintResponse {
87    pub valid: bool,
88    #[builder(default)]
89    pub merged_yaml: String,
90    pub errors: Vec<String>,
91}
92
93impl LintResponse {
94    pub fn builder() -> LintResponseBuilder {
95        LintResponseBuilder::default()
96    }
97}
98
99pub struct YamlBytes<'a>(&'a [u8]);
100
101impl YamlBytes<'_> {
102    pub fn new(data: &[u8]) -> YamlBytes<'_> {
103        YamlBytes(data)
104    }
105}
106
107impl Display for YamlBytes<'_> {
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        let s = String::from_utf8_lossy(self.0);
110        write!(f, "{s}")
111    }
112}
113
114#[derive(Builder, Clone)]
115pub struct Runner {
116    pub id: i64,
117    pub active: bool,
118    pub description: String,
119    pub ip_address: String,
120    pub name: String,
121    pub online: bool,
122    pub paused: bool,
123    pub is_shared: bool,
124    pub runner_type: String,
125    pub status: String,
126}
127
128impl Runner {
129    pub fn builder() -> RunnerBuilder {
130        RunnerBuilder::default()
131    }
132}
133
134impl From<Runner> for DisplayBody {
135    fn from(r: Runner) -> DisplayBody {
136        DisplayBody {
137            columns: vec![
138                Column::new("ID", r.id.to_string()),
139                Column::new("Active", r.active.to_string()),
140                Column::new("Description", r.description),
141                Column::new("IP Address", r.ip_address),
142                Column::new("Name", r.name),
143                Column::new("Paused", r.paused.to_string()),
144                Column::new("Shared", r.is_shared.to_string()),
145                Column::new("Type", r.runner_type),
146                Column::new("Online", r.online.to_string()),
147                Column::new("Status", r.status.to_string()),
148            ],
149        }
150    }
151}
152
153impl Timestamp for Runner {
154    fn created_at(&self) -> String {
155        // There is no created_at field for runners, set it to UNIX epoch
156        "1970-01-01T00:00:00Z".to_string()
157    }
158}
159
160/// Used when getting runner details. Adds extra fields to the runner struct.
161#[derive(Builder, Clone)]
162pub struct RunnerMetadata {
163    pub id: i64,
164    pub run_untagged: bool,
165    pub tag_list: Vec<String>,
166    pub version: String,
167    pub architecture: String,
168    pub platform: String,
169    pub contacted_at: String,
170    pub revision: String,
171}
172
173impl RunnerMetadata {
174    pub fn builder() -> RunnerMetadataBuilder {
175        RunnerMetadataBuilder::default()
176    }
177}
178
179impl From<RunnerMetadata> for DisplayBody {
180    fn from(r: RunnerMetadata) -> DisplayBody {
181        DisplayBody {
182            columns: vec![
183                Column::new("ID", r.id.to_string()),
184                Column::new("Run untagged", r.run_untagged.to_string()),
185                Column::new("Tags", r.tag_list.join(", ")),
186                Column::new("Architecture", r.architecture),
187                Column::new("Platform", r.platform),
188                Column::new("Contacted at", r.contacted_at),
189                Column::new("Version", r.version),
190                Column::new("Revision", r.revision),
191            ],
192        }
193    }
194}
195
196#[derive(Builder, Clone)]
197pub struct RunnerListCliArgs {
198    pub status: RunnerStatus,
199    #[builder(default)]
200    pub tags: Option<String>,
201    #[builder(default)]
202    pub all: bool,
203    pub list_args: ListRemoteCliArgs,
204}
205
206impl RunnerListCliArgs {
207    pub fn builder() -> RunnerListCliArgsBuilder {
208        RunnerListCliArgsBuilder::default()
209    }
210}
211
212#[derive(Builder, Clone)]
213pub struct RunnerListBodyArgs {
214    pub list_args: Option<ListBodyArgs>,
215    pub status: RunnerStatus,
216    #[builder(default)]
217    pub tags: Option<String>,
218    #[builder(default)]
219    pub all: bool,
220}
221
222impl RunnerListBodyArgs {
223    pub fn builder() -> RunnerListBodyArgsBuilder {
224        RunnerListBodyArgsBuilder::default()
225    }
226}
227
228#[derive(Builder, Clone)]
229pub struct RunnerMetadataGetCliArgs {
230    pub id: i64,
231    pub get_args: GetRemoteCliArgs,
232}
233
234impl RunnerMetadataGetCliArgs {
235    pub fn builder() -> RunnerMetadataGetCliArgsBuilder {
236        RunnerMetadataGetCliArgsBuilder::default()
237    }
238}
239
240#[derive(Builder, Clone)]
241pub struct RunnerPostDataCliArgs {
242    pub description: Option<String>,
243    pub tags: Option<String>,
244    pub kind: RunnerType,
245    #[builder(default)]
246    pub run_untagged: bool,
247    #[builder(default)]
248    pub project_id: Option<i64>,
249    #[builder(default)]
250    pub group_id: Option<i64>,
251}
252
253impl RunnerPostDataCliArgs {
254    pub fn builder() -> RunnerPostDataCliArgsBuilder {
255        RunnerPostDataCliArgsBuilder::default()
256    }
257}
258
259fn create_runner<W: Write>(
260    remote: Arc<dyn CicdRunner>,
261    cli_args: RunnerPostDataCliArgs,
262    mut writer: W,
263) -> Result<()> {
264    let response = remote.create(cli_args)?;
265    writeln!(writer, "{response}")?;
266    Ok(())
267}
268
269#[derive(Builder, Clone)]
270pub struct RunnerRegistrationResponse {
271    pub id: i64,
272    pub token: String,
273    pub token_expiration: String,
274}
275
276impl RunnerRegistrationResponse {
277    pub fn builder() -> RunnerRegistrationResponseBuilder {
278        RunnerRegistrationResponseBuilder::default()
279    }
280}
281
282impl Display for RunnerRegistrationResponse {
283    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284        write!(
285            f,
286            "Runner ID: [{}], Runner Token: [{}], Token Expiration: [{}]",
287            self.id, self.token, self.token_expiration
288        )
289    }
290}
291
292#[derive(Clone, PartialEq, Debug)]
293pub enum RunnerType {
294    Instance,
295    Group,
296    Project,
297}
298
299impl Display for RunnerType {
300    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301        match self {
302            RunnerType::Instance => write!(f, "instance_type"),
303            RunnerType::Group => write!(f, "group_type"),
304            RunnerType::Project => write!(f, "project_type"),
305        }
306    }
307}
308
309#[derive(Clone, Copy, PartialEq, Debug)]
310pub enum RunnerStatus {
311    Online,
312    Offline,
313    Stale,
314    NeverContacted,
315    All,
316}
317
318impl Display for RunnerStatus {
319    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        match self {
321            RunnerStatus::Online => write!(f, "online"),
322            RunnerStatus::Offline => write!(f, "offline"),
323            RunnerStatus::Stale => write!(f, "stale"),
324            RunnerStatus::NeverContacted => write!(f, "never_contacted"),
325            RunnerStatus::All => write!(f, "all"),
326        }
327    }
328}
329
330#[derive(Builder, Clone)]
331pub struct Job {
332    id: i64,
333    name: String,
334    branch: String,
335    url: String,
336    author_name: String,
337    commit_sha: String,
338    pipeline_id: i64,
339    runner_tags: Vec<String>,
340    stage: String,
341    status: String,
342    created_at: String,
343    started_at: String,
344    finished_at: String,
345    duration: String,
346}
347
348impl Job {
349    pub fn builder() -> JobBuilder {
350        JobBuilder::default()
351    }
352}
353
354impl From<Job> for DisplayBody {
355    fn from(j: Job) -> DisplayBody {
356        DisplayBody {
357            columns: vec![
358                Column::new("ID", j.id.to_string()),
359                Column::new("Name", j.name),
360                Column::new("Author Name", j.author_name),
361                Column::new("Branch", j.branch),
362                Column::new("Commit SHA", j.commit_sha),
363                Column::new("Pipeline ID", j.pipeline_id.to_string()),
364                Column::new("URL", j.url),
365                Column::new("Runner Tags", j.runner_tags.join(", ")),
366                Column::new("Stage", j.stage),
367                Column::new("Status", j.status),
368                Column::new("Created At", j.created_at),
369                Column::new("Started At", j.started_at),
370                Column::new("Finished At", j.finished_at),
371                Column::new("Duration", j.duration.to_string()),
372            ],
373        }
374    }
375}
376
377impl Timestamp for Job {
378    fn created_at(&self) -> String {
379        self.created_at.clone()
380    }
381}
382
383// Technically no need to encapsulate the common ListRemoteCliArgs but we might
384// need to add pipeline_id to retrieve jobs from a specific pipeline.
385#[derive(Builder, Clone)]
386pub struct JobListCliArgs {
387    pub list_args: ListRemoteCliArgs,
388}
389
390impl JobListCliArgs {
391    pub fn builder() -> JobListCliArgsBuilder {
392        JobListCliArgsBuilder::default()
393    }
394}
395
396#[derive(Builder, Clone)]
397pub struct JobListBodyArgs {
398    pub list_args: Option<ListBodyArgs>,
399}
400
401impl JobListBodyArgs {
402    pub fn builder() -> JobListBodyArgsBuilder {
403        JobListBodyArgsBuilder::default()
404    }
405}
406
407pub fn execute(
408    options: PipelineOptions,
409    config: Arc<dyn ConfigProperties>,
410    domain: String,
411    path: String,
412) -> Result<()> {
413    match options {
414        PipelineOptions::Lint(args) => {
415            // TODO - should propagage cache args
416            let remote = remote::get_cicd(domain, path, config, None, CacheType::File)?;
417            let file = std::fs::File::open(args.path)?;
418            let body = read_ci_file(file)?;
419            lint_ci_file(remote, &body, false, std::io::stdout())
420        }
421        PipelineOptions::MergedCi => {
422            // TODO - should propagage cache args
423            let remote = remote::get_cicd(domain, path, config, None, CacheType::File)?;
424            let file = std::fs::File::open(".gitlab-ci.yml")?;
425            let body = read_ci_file(file)?;
426            lint_ci_file(remote, &body, true, std::io::stdout())
427        }
428        PipelineOptions::Chart(args) => {
429            let file = std::fs::File::open(".gitlab-ci.yml")?;
430            let body = read_ci_file(file)?;
431            let parser = YamlParser::new(load_yaml(&String::from_utf8_lossy(&body)));
432            let chart = generate_mermaid_stages_diagram(parser, args)?;
433            println!("{chart}");
434            Ok(())
435        }
436        PipelineOptions::List(cli_args) => {
437            let remote = remote::get_cicd(
438                domain,
439                path,
440                config,
441                Some(&cli_args.get_args.cache_args),
442                CacheType::File,
443            )?;
444            if cli_args.num_pages {
445                return num_cicd_pages(remote, std::io::stdout());
446            } else if cli_args.num_resources {
447                return num_cicd_resources(remote, std::io::stdout());
448            }
449            let from_to_args = remote::validate_from_to_page(&cli_args)?;
450            let body_args = PipelineBodyArgs::builder()
451                .from_to_page(from_to_args)
452                .build()?;
453            list_pipelines(remote, body_args, cli_args, std::io::stdout())
454        }
455        PipelineOptions::Jobs(options) => match options {
456            JobOptions::List(cli_args) => {
457                let remote = remote::get_cicd_job(
458                    domain,
459                    path,
460                    config,
461                    Some(&cli_args.list_args.get_args.cache_args),
462                    CacheType::File,
463                )?;
464                let from_to_args = remote::validate_from_to_page(&cli_args.list_args)?;
465                let body_args = JobListBodyArgs::builder().list_args(from_to_args).build()?;
466                if cli_args.list_args.num_pages {
467                    return num_job_pages(remote, body_args, std::io::stdout());
468                }
469                if cli_args.list_args.num_resources {
470                    return num_job_resources(remote, body_args, std::io::stdout());
471                }
472                list_jobs(remote, body_args, cli_args, std::io::stdout())
473            }
474        },
475        PipelineOptions::Runners(options) => match options {
476            RunnerOptions::List(cli_args) => {
477                let remote = remote::get_cicd_runner(
478                    domain,
479                    path,
480                    config,
481                    Some(&cli_args.list_args.get_args.cache_args),
482                    CacheType::File,
483                )?;
484                let from_to_args = remote::validate_from_to_page(&cli_args.list_args)?;
485                let tags = cli_args.tags.clone();
486                let body_args = RunnerListBodyArgs::builder()
487                    .list_args(from_to_args)
488                    .status(cli_args.status)
489                    .tags(tags)
490                    .all(cli_args.all)
491                    .build()?;
492                if cli_args.list_args.num_pages {
493                    return num_runner_pages(remote, body_args, std::io::stdout());
494                }
495                if cli_args.list_args.num_resources {
496                    return num_runner_resources(remote, body_args, std::io::stdout());
497                }
498                list_runners(remote, body_args, cli_args, std::io::stdout())
499            }
500            RunnerOptions::Get(cli_args) => {
501                let remote = remote::get_cicd_runner(
502                    domain,
503                    path,
504                    config,
505                    Some(&cli_args.get_args.cache_args),
506                    CacheType::File,
507                )?;
508                get_runner_details(remote, cli_args, std::io::stdout())
509            }
510            RunnerOptions::Create(cli_args) => {
511                let remote = remote::get_cicd_runner(domain, path, config, None, CacheType::None)?;
512                create_runner(remote, cli_args, std::io::stdout())
513            }
514        },
515    }
516}
517
518fn get_runner_details<W: Write>(
519    remote: Arc<dyn CicdRunner>,
520    cli_args: RunnerMetadataGetCliArgs,
521    mut writer: W,
522) -> Result<()> {
523    let runner = remote.get(cli_args.id)?;
524    display::print(&mut writer, vec![runner], cli_args.get_args)?;
525    Ok(())
526}
527
528fn list_runners<W: Write>(
529    remote: Arc<dyn CicdRunner>,
530    body_args: RunnerListBodyArgs,
531    cli_args: RunnerListCliArgs,
532    mut writer: W,
533) -> Result<()> {
534    common::list_runners(remote, body_args, cli_args, &mut writer)
535}
536
537fn list_jobs<W: Write>(
538    remote: Arc<dyn CicdJob>,
539    body_args: JobListBodyArgs,
540    cli_args: JobListCliArgs,
541    mut writer: W,
542) -> Result<()> {
543    common::list_jobs(remote, body_args, cli_args, &mut writer)
544}
545
546fn list_pipelines<W: Write>(
547    remote: Arc<dyn Cicd>,
548    body_args: PipelineBodyArgs,
549    cli_args: ListRemoteCliArgs,
550    mut writer: W,
551) -> Result<()> {
552    common::list_pipelines(remote, body_args, cli_args, &mut writer)
553}
554
555fn read_ci_file<R: Read>(mut reader: R) -> Result<Vec<u8>> {
556    let mut buf = Vec::new();
557    reader.read_to_end(&mut buf)?;
558    Ok(buf)
559}
560
561fn lint_ci_file<W: Write>(
562    remote: Arc<dyn Cicd>,
563    body: &[u8],
564    display_merged_ci_yaml: bool,
565    mut writer: W,
566) -> Result<()> {
567    let response = remote.lint(YamlBytes::new(body))?;
568    if response.valid {
569        if display_merged_ci_yaml {
570            let lines = response.merged_yaml.split('\n');
571            for line in lines {
572                if line.is_empty() {
573                    continue;
574                }
575                writeln!(writer, "{line}")?;
576            }
577            return Ok(());
578        }
579        writeln!(writer, "File is valid.")?;
580    } else {
581        for error in response.errors {
582            writeln!(writer, "{error}")?;
583        }
584        return Err(error::gen("Linting failed."));
585    }
586    Ok(())
587}
588
589#[cfg(test)]
590mod test {
591    use std::io::Cursor;
592
593    use super::*;
594    use crate::{api_traits::NumberDeltaErr, error};
595
596    #[derive(Clone, Builder)]
597    struct PipelineMock {
598        #[builder(default = "vec![]")]
599        pipelines: Vec<Pipeline>,
600        #[builder(default = "false")]
601        error: bool,
602        #[builder(setter(into, strip_option), default)]
603        num_pages: Option<u32>,
604        #[builder(default)]
605        gitlab_ci_merged_yaml: String,
606    }
607
608    impl PipelineMock {
609        pub fn builder() -> PipelineMockBuilder {
610            PipelineMockBuilder::default()
611        }
612    }
613
614    impl Cicd for PipelineMock {
615        fn list(&self, _args: PipelineBodyArgs) -> Result<Vec<Pipeline>> {
616            if self.error {
617                return Err(error::gen("Error"));
618            }
619            let pp = self.pipelines.clone();
620            Ok(pp)
621        }
622
623        fn get_pipeline(&self, _id: i64) -> Result<Pipeline> {
624            let pp = self.pipelines.clone();
625            Ok(pp[0].clone())
626        }
627
628        fn num_pages(&self) -> Result<Option<u32>> {
629            if self.error {
630                return Err(error::gen("Error"));
631            }
632            Ok(self.num_pages)
633        }
634
635        fn num_resources(&self) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
636            todo!()
637        }
638
639        fn lint(&self, _body: YamlBytes) -> Result<LintResponse> {
640            if self.error {
641                return Ok(LintResponse::builder()
642                    .valid(false)
643                    .errors(vec!["YAML Error".to_string()])
644                    .build()
645                    .unwrap());
646            }
647            Ok(LintResponse::builder()
648                .valid(true)
649                .errors(vec![])
650                .merged_yaml(self.gitlab_ci_merged_yaml.clone())
651                .build()
652                .unwrap())
653        }
654    }
655
656    #[test]
657    fn test_list_pipelines() {
658        let pp_remote = PipelineMock::builder()
659            .pipelines(vec![
660                Pipeline::builder()
661                    .id(123)
662                    .status("success".to_string())
663                    .web_url("https://gitlab.com/owner/repo/-/pipelines/123".to_string())
664                    .branch("master".to_string())
665                    .sha("1234567890abcdef".to_string())
666                    .created_at("2020-01-01T00:00:00Z".to_string())
667                    .updated_at("2020-01-01T00:01:00Z".to_string())
668                    .duration(60)
669                    .build()
670                    .unwrap(),
671                Pipeline::builder()
672                    .id(456)
673                    .status("failed".to_string())
674                    .web_url("https://gitlab.com/owner/repo/-/pipelines/456".to_string())
675                    .branch("master".to_string())
676                    .sha("1234567890abcdef".to_string())
677                    .created_at("2020-01-01T00:00:00Z".to_string())
678                    .updated_at("2020-01-01T00:01:01Z".to_string())
679                    .duration(61)
680                    .build()
681                    .unwrap(),
682            ])
683            .build()
684            .unwrap();
685        let mut buf = Vec::new();
686        let body_args = PipelineBodyArgs::builder()
687            .from_to_page(None)
688            .build()
689            .unwrap();
690        let cli_args = ListRemoteCliArgs::builder().build().unwrap();
691        list_pipelines(Arc::new(pp_remote), body_args, cli_args, &mut buf).unwrap();
692        assert_eq!(
693            String::from_utf8(buf).unwrap(),
694            "ID|URL|Branch|SHA|Created at|Updated at|Duration|Status\n\
695             123|https://gitlab.com/owner/repo/-/pipelines/123|master|1234567890abcdef|2020-01-01T00:00:00Z|2020-01-01T00:01:00Z|60|success\n\
696             456|https://gitlab.com/owner/repo/-/pipelines/456|master|1234567890abcdef|2020-01-01T00:00:00Z|2020-01-01T00:01:01Z|61|failed\n")
697    }
698
699    #[test]
700    fn test_list_pipelines_empty_warns_message() {
701        let pp_remote = PipelineMock::builder().build().unwrap();
702        let mut buf = Vec::new();
703
704        let body_args = PipelineBodyArgs::builder()
705            .from_to_page(None)
706            .build()
707            .unwrap();
708        let cli_args = ListRemoteCliArgs::builder().build().unwrap();
709        list_pipelines(Arc::new(pp_remote), body_args, cli_args, &mut buf).unwrap();
710        assert_eq!("No resources found.\n", String::from_utf8(buf).unwrap(),)
711    }
712
713    #[test]
714    fn test_pipelines_empty_with_flush_option_no_warn_message() {
715        let pp_remote = PipelineMock::builder().build().unwrap();
716        let mut buf = Vec::new();
717        let body_args = PipelineBodyArgs::builder()
718            .from_to_page(None)
719            .build()
720            .unwrap();
721        let cli_args = ListRemoteCliArgs::builder().flush(true).build().unwrap();
722        list_pipelines(Arc::new(pp_remote), body_args, cli_args, &mut buf).unwrap();
723        assert_eq!("", String::from_utf8(buf).unwrap(),)
724    }
725
726    #[test]
727    fn test_list_pipelines_error() {
728        let pp_remote = PipelineMock::builder().error(true).build().unwrap();
729        let mut buf = Vec::new();
730        let body_args = PipelineBodyArgs::builder()
731            .from_to_page(None)
732            .build()
733            .unwrap();
734        let cli_args = ListRemoteCliArgs::builder().build().unwrap();
735        assert!(list_pipelines(Arc::new(pp_remote), body_args, cli_args, &mut buf).is_err());
736    }
737
738    #[test]
739    fn test_list_number_of_pipelines_pages() {
740        let pp_remote = PipelineMock::builder().num_pages(3_u32).build().unwrap();
741        let mut buf = Vec::new();
742        num_cicd_pages(Arc::new(pp_remote), &mut buf).unwrap();
743        assert_eq!("3\n", String::from_utf8(buf).unwrap(),)
744    }
745
746    #[test]
747    fn test_no_pages_available() {
748        let pp_remote = PipelineMock::builder().build().unwrap();
749        let mut buf = Vec::new();
750        num_cicd_pages(Arc::new(pp_remote), &mut buf).unwrap();
751        assert_eq!(
752            "Number of pages not available.\n",
753            String::from_utf8(buf).unwrap(),
754        )
755    }
756
757    #[test]
758    fn test_number_of_pages_error() {
759        let pp_remote = PipelineMock::builder().error(true).build().unwrap();
760        let mut buf = Vec::new();
761        assert!(num_cicd_pages(Arc::new(pp_remote), &mut buf).is_err());
762    }
763
764    #[test]
765    fn test_list_pipelines_no_headers() {
766        let pp_remote = PipelineMock::builder()
767            .pipelines(vec![
768                Pipeline::builder()
769                    .id(123)
770                    .status("success".to_string())
771                    .web_url("https://gitlab.com/owner/repo/-/pipelines/123".to_string())
772                    .branch("master".to_string())
773                    .sha("1234567890abcdef".to_string())
774                    .created_at("2020-01-01T00:00:00Z".to_string())
775                    .updated_at("2020-01-01T00:01:00Z".to_string())
776                    .duration(60)
777                    .build()
778                    .unwrap(),
779                Pipeline::builder()
780                    .id(456)
781                    .status("failed".to_string())
782                    .web_url("https://gitlab.com/owner/repo/-/pipelines/456".to_string())
783                    .branch("master".to_string())
784                    .sha("1234567890abcdef".to_string())
785                    .created_at("2020-01-01T00:00:00Z".to_string())
786                    .updated_at("2020-01-01T00:01:00Z".to_string())
787                    .duration(60)
788                    .build()
789                    .unwrap(),
790            ])
791            .build()
792            .unwrap();
793        let mut buf = Vec::new();
794        let body_args = PipelineBodyArgs::builder()
795            .from_to_page(None)
796            .build()
797            .unwrap();
798        let cli_args = ListRemoteCliArgs::builder()
799            .get_args(
800                GetRemoteCliArgs::builder()
801                    .no_headers(true)
802                    .build()
803                    .unwrap(),
804            )
805            .build()
806            .unwrap();
807        list_pipelines(Arc::new(pp_remote), body_args, cli_args, &mut buf).unwrap();
808        assert_eq!(
809            "123|https://gitlab.com/owner/repo/-/pipelines/123|master|1234567890abcdef|2020-01-01T00:00:00Z|2020-01-01T00:01:00Z|60|success\n\
810             456|https://gitlab.com/owner/repo/-/pipelines/456|master|1234567890abcdef|2020-01-01T00:00:00Z|2020-01-01T00:01:00Z|60|failed\n",
811            String::from_utf8(buf).unwrap(),
812        )
813    }
814
815    #[derive(Builder, Clone)]
816    struct RunnerMock {
817        #[builder(default = "vec![]")]
818        runners: Vec<Runner>,
819        #[builder(default)]
820        error: bool,
821        #[builder(default)]
822        one_runner: Option<RunnerMetadata>,
823    }
824
825    impl RunnerMock {
826        pub fn builder() -> RunnerMockBuilder {
827            RunnerMockBuilder::default()
828        }
829    }
830
831    impl CicdRunner for RunnerMock {
832        fn list(&self, _args: RunnerListBodyArgs) -> Result<Vec<Runner>> {
833            if self.error {
834                return Err(error::gen("Error"));
835            }
836            let rr = self.runners.clone();
837            Ok(rr)
838        }
839
840        fn get(&self, _id: i64) -> Result<RunnerMetadata> {
841            let rr = self.one_runner.as_ref().unwrap();
842            Ok(rr.clone())
843        }
844
845        fn num_pages(&self, _args: RunnerListBodyArgs) -> Result<Option<u32>> {
846            if self.error {
847                return Err(error::gen("Error"));
848            }
849            Ok(None)
850        }
851
852        fn num_resources(
853            &self,
854            _args: RunnerListBodyArgs,
855        ) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
856            todo!()
857        }
858
859        fn create(&self, _args: RunnerPostDataCliArgs) -> Result<RunnerRegistrationResponse> {
860            Ok(RunnerRegistrationResponse::builder()
861                .id(1)
862                .token("token".to_string())
863                .token_expiration("2020-01-01T00:00:00Z".to_string())
864                .build()
865                .unwrap())
866        }
867    }
868
869    #[test]
870    fn test_list_runners() {
871        let runners = vec![
872            Runner::builder()
873                .id(1)
874                .active(true)
875                .description("Runner 1".to_string())
876                .ip_address("10.0.0.1".to_string())
877                .name("runner1".to_string())
878                .online(true)
879                .status("online".to_string())
880                .paused(false)
881                .is_shared(true)
882                .runner_type("shared".to_string())
883                .build()
884                .unwrap(),
885            Runner::builder()
886                .id(2)
887                .active(true)
888                .description("Runner 2".to_string())
889                .ip_address("10.0.0.2".to_string())
890                .name("runner2".to_string())
891                .online(true)
892                .status("online".to_string())
893                .paused(false)
894                .is_shared(true)
895                .runner_type("shared".to_string())
896                .build()
897                .unwrap(),
898        ];
899        let remote = RunnerMock::builder().runners(runners).build().unwrap();
900        let mut buf = Vec::new();
901        let body_args = RunnerListBodyArgs::builder()
902            .list_args(None)
903            .status(RunnerStatus::Online)
904            .build()
905            .unwrap();
906        let cli_args = RunnerListCliArgs::builder()
907            .status(RunnerStatus::Online)
908            .list_args(ListRemoteCliArgs::builder().build().unwrap())
909            .build()
910            .unwrap();
911        list_runners(Arc::new(remote), body_args, cli_args, &mut buf).unwrap();
912        assert_eq!(
913            "ID|Active|Description|IP Address|Name|Paused|Shared|Type|Online|Status\n\
914             1|true|Runner 1|10.0.0.1|runner1|false|true|shared|true|online\n\
915             2|true|Runner 2|10.0.0.2|runner2|false|true|shared|true|online\n",
916            String::from_utf8(buf).unwrap()
917        )
918    }
919
920    #[test]
921    fn test_no_runners_warn_user_with_message() {
922        let remote = RunnerMock::builder().build().unwrap();
923        let mut buf = Vec::new();
924        let body_args = RunnerListBodyArgs::builder()
925            .list_args(None)
926            .status(RunnerStatus::Online)
927            .build()
928            .unwrap();
929        let cli_args = RunnerListCliArgs::builder()
930            .status(RunnerStatus::Online)
931            .list_args(ListRemoteCliArgs::builder().build().unwrap())
932            .build()
933            .unwrap();
934        list_runners(Arc::new(remote), body_args, cli_args, &mut buf).unwrap();
935        assert_eq!("No resources found.\n", String::from_utf8(buf).unwrap())
936    }
937
938    #[test]
939    fn test_no_runners_found_with_flush_option_no_warn_message() {
940        let remote = RunnerMock::builder().build().unwrap();
941        let mut buf = Vec::new();
942        let body_args = RunnerListBodyArgs::builder()
943            .list_args(None)
944            .status(RunnerStatus::Online)
945            .build()
946            .unwrap();
947        let cli_args = RunnerListCliArgs::builder()
948            .status(RunnerStatus::Online)
949            .list_args(ListRemoteCliArgs::builder().flush(true).build().unwrap())
950            .build()
951            .unwrap();
952        list_runners(Arc::new(remote), body_args, cli_args, &mut buf).unwrap();
953        assert_eq!("", String::from_utf8(buf).unwrap())
954    }
955
956    #[test]
957    fn test_get_gitlab_runner_metadata() {
958        let runner_metadata = RunnerMetadata::builder()
959            .id(1)
960            .run_untagged(true)
961            .tag_list(vec!["tag1".to_string(), "tag2".to_string()])
962            .version("13.0.0".to_string())
963            .architecture("amd64".to_string())
964            .platform("linux".to_string())
965            .contacted_at("2020-01-01T00:00:00Z".to_string())
966            .revision("1234567890abcdef".to_string())
967            .build()
968            .unwrap();
969        let remote = RunnerMock::builder()
970            .one_runner(Some(runner_metadata))
971            .build()
972            .unwrap();
973        let mut buf = Vec::new();
974        let cli_args = RunnerMetadataGetCliArgs::builder()
975            .id(1)
976            .get_args(GetRemoteCliArgs::builder().build().unwrap())
977            .build()
978            .unwrap();
979        get_runner_details(Arc::new(remote), cli_args, &mut buf).unwrap();
980        assert_eq!(
981            "ID|Run untagged|Tags|Architecture|Platform|Contacted at|Version|Revision\n\
982             1|true|tag1, tag2|amd64|linux|2020-01-01T00:00:00Z|13.0.0|1234567890abcdef\n",
983            String::from_utf8(buf).unwrap()
984        )
985    }
986
987    fn gen_gitlab_ci_body() -> Vec<u8> {
988        b"image: alpine\n\
989          stages:\n\
990            - build\n\
991            - test\n\
992          build:\n\
993            stage: build\n\
994            script:\n\
995              - echo \"Building\"\n\
996          test:\n\
997            stage: test\n\
998            script:\n\
999              - echo \"Testing\"\n"
1000            .to_vec()
1001    }
1002
1003    #[test]
1004    fn test_read_gitlab_ci_file_contents() {
1005        let expected_body = gen_gitlab_ci_body();
1006        let buf = Cursor::new(&expected_body);
1007        let body = read_ci_file(buf).unwrap();
1008        assert_eq!(*expected_body, *body);
1009    }
1010
1011    #[test]
1012    fn test_lint_ci_file_success() {
1013        let mock_cicd = Arc::new(PipelineMock::builder().build().unwrap());
1014        let mut writer = Vec::new();
1015        let result = lint_ci_file(mock_cicd, &gen_gitlab_ci_body(), false, &mut writer);
1016        assert!(result.is_ok());
1017        assert_eq!(String::from_utf8(writer).unwrap(), "File is valid.\n");
1018    }
1019
1020    #[test]
1021    fn test_lint_ci_file_has_errors_prints_errors() {
1022        let mock_cicd = Arc::new(PipelineMock::builder().error(true).build().unwrap());
1023        let mut writer = Vec::new();
1024        let result = lint_ci_file(mock_cicd, &gen_gitlab_ci_body(), false, &mut writer);
1025        assert!(result.is_err());
1026        assert_eq!(String::from_utf8(writer).unwrap(), "YAML Error\n");
1027    }
1028
1029    #[test]
1030    fn test_get_merged_yaml_from_lint_response() {
1031        let response = LintResponse::builder()
1032            .valid(true)
1033            .merged_yaml("image: alpine\nstages:\n  - build\n  - test\nbuild:\n  stage: build\n  script:\n  - echo \"Building\"\ntest:\n  stage: test\n  script:\n  - echo \"Testing\"\n".to_string())
1034            .errors(vec![])
1035            .build()
1036            .unwrap();
1037        let mut writer = Vec::new();
1038        let mock_cicd = Arc::new(
1039            PipelineMock::builder()
1040                .gitlab_ci_merged_yaml(response.merged_yaml)
1041                .build()
1042                .unwrap(),
1043        );
1044
1045        let result = lint_ci_file(mock_cicd, &gen_gitlab_ci_body(), true, &mut writer);
1046        assert!(result.is_ok());
1047        let merged_gitlab_ci = r#"image: alpine
1048stages:
1049  - build
1050  - test
1051build:
1052  stage: build
1053  script:
1054  - echo "Building"
1055test:
1056  stage: test
1057  script:
1058  - echo "Testing"
1059"#;
1060        assert_eq!(merged_gitlab_ci, String::from_utf8(writer).unwrap());
1061    }
1062
1063    #[derive(Builder)]
1064    struct JobMock {
1065        #[builder(default)]
1066        jobs: Vec<Job>,
1067        #[builder(default)]
1068        error: bool,
1069        #[builder(default)]
1070        num_pages: Option<u32>,
1071    }
1072
1073    impl JobMock {
1074        pub fn builder() -> JobMockBuilder {
1075            JobMockBuilder::default()
1076        }
1077    }
1078
1079    impl CicdJob for JobMock {
1080        fn list(&self, _args: JobListBodyArgs) -> Result<Vec<Job>> {
1081            if self.error {
1082                return Err(error::gen("Error"));
1083            }
1084            let jj = self.jobs.clone();
1085            Ok(jj)
1086        }
1087
1088        fn num_pages(&self, _args: JobListBodyArgs) -> Result<Option<u32>> {
1089            if self.error {
1090                return Err(error::gen("Error"));
1091            }
1092            Ok(self.num_pages)
1093        }
1094
1095        fn num_resources(&self, _args: JobListBodyArgs) -> Result<Option<NumberDeltaErr>> {
1096            todo!()
1097        }
1098    }
1099
1100    #[test]
1101    fn test_list_pipeline_jobs() {
1102        let jobs = vec![
1103            Job::builder()
1104                .id(1)
1105                .name("job1".to_string())
1106                .branch("main".to_string())
1107                .author_name("user1".to_string())
1108                .commit_sha("1234567890abcdef".to_string())
1109                .pipeline_id(1)
1110                .url("https://gitlab.com/owner/repo/-/jobs/1".to_string())
1111                .runner_tags(vec!["tag1".to_string(), "tag2".to_string()])
1112                .stage("build".to_string())
1113                .status("success".to_string())
1114                .created_at("2020-01-01T00:00:00Z".to_string())
1115                .started_at("2020-01-01T00:01:00Z".to_string())
1116                .finished_at("2020-01-01T00:01:30Z".to_string())
1117                .duration("25".to_string())
1118                .build()
1119                .unwrap(),
1120            Job::builder()
1121                .id(2)
1122                .name("job2".to_string())
1123                .branch("main".to_string())
1124                .author_name("user2".to_string())
1125                .commit_sha("1234567890abcdef".to_string())
1126                .pipeline_id(1)
1127                .url("https://gitlab.com/owner/repo/-/jobs/2".to_string())
1128                .runner_tags(vec!["tag1".to_string(), "tag2".to_string()])
1129                .stage("test".to_string())
1130                .status("failed".to_string())
1131                .created_at("2020-01-01T00:00:00Z".to_string())
1132                .started_at("2020-01-01T00:01:00Z".to_string())
1133                .finished_at("2020-01-01T00:01:30Z".to_string())
1134                .duration("30".to_string())
1135                .build()
1136                .unwrap(),
1137        ];
1138        let remote = JobMock::builder().jobs(jobs).build().unwrap();
1139        let mut buf = Vec::new();
1140        let body_args = JobListBodyArgs::builder().list_args(None).build().unwrap();
1141        let cli_args = JobListCliArgs::builder()
1142            .list_args(ListRemoteCliArgs::builder().build().unwrap())
1143            .build()
1144            .unwrap();
1145        list_jobs(Arc::new(remote), body_args, cli_args, &mut buf).unwrap();
1146        assert_eq!(
1147"ID|Name|Author Name|Branch|Commit SHA|Pipeline ID|URL|Runner Tags|Stage|Status|Created At|Started At|Finished At|Duration\n1|job1|user1|main|1234567890abcdef|1|https://gitlab.com/owner/repo/-/jobs/1|tag1, tag2|build|success|2020-01-01T00:00:00Z|2020-01-01T00:01:00Z|2020-01-01T00:01:30Z|25\n2|job2|user2|main|1234567890abcdef|1|https://gitlab.com/owner/repo/-/jobs/2|tag1, tag2|test|failed|2020-01-01T00:00:00Z|2020-01-01T00:01:00Z|2020-01-01T00:01:30Z|30\n",
1148            String::from_utf8(buf).unwrap()
1149        );
1150    }
1151
1152    #[test]
1153    fn test_create_new_runner() {
1154        let remote = RunnerMock::builder().build().unwrap();
1155        let mut buf = Vec::new();
1156        let cli_args = RunnerPostDataCliArgs::builder()
1157            .description(Some("Runner 1".to_string()))
1158            .tags(Some("tag1,tag2".to_string()))
1159            .kind(RunnerType::Instance)
1160            .build()
1161            .unwrap();
1162        create_runner(Arc::new(remote), cli_args, &mut buf).unwrap();
1163        assert_eq!(
1164            "Runner ID: [1], Runner Token: [token], Token Expiration: [2020-01-01T00:00:00Z]\n",
1165            String::from_utf8(buf).unwrap()
1166        )
1167    }
1168}