gr/github/
cicd.rs

1use super::Github;
2use crate::api_traits::{ApiOperation, CicdJob, CicdRunner, NumberDeltaErr};
3use crate::cmds::cicd::{
4    Job, JobListBodyArgs, LintResponse, Pipeline, PipelineBodyArgs, RunnerListBodyArgs,
5    RunnerMetadata, RunnerPostDataCliArgs, RunnerRegistrationResponse, YamlBytes,
6};
7use crate::remote::query;
8use crate::{
9    api_traits::Cicd,
10    io::{HttpResponse, HttpRunner},
11};
12use crate::{http, time, Result};
13
14impl<R: HttpRunner<Response = HttpResponse>> Cicd for Github<R> {
15    fn list(&self, args: PipelineBodyArgs) -> Result<Vec<Pipeline>> {
16        // Doc:
17        // https://docs.github.com/en/rest/actions/workflow-runs?apiVersion=2022-11-28#list-workflow-runs-for-a-repository
18        let url = format!(
19            "{}/repos/{}/actions/runs",
20            self.rest_api_basepath, self.path
21        );
22        query::paged(
23            &self.runner,
24            &url,
25            args.from_to_page,
26            self.request_headers(),
27            Some("workflow_runs"),
28            ApiOperation::Pipeline,
29            |value| GithubPipelineFields::from(value).into(),
30        )
31    }
32
33    fn get_pipeline(&self, _id: i64) -> Result<Pipeline> {
34        todo!()
35    }
36
37    fn num_pages(&self) -> Result<Option<u32>> {
38        let (url, headers) = self.resource_cicd_metadata_url();
39        query::num_pages(&self.runner, &url, headers, ApiOperation::Pipeline)
40    }
41
42    fn num_resources(&self) -> Result<Option<NumberDeltaErr>> {
43        let (url, headers) = self.resource_cicd_metadata_url();
44        query::num_resources(&self.runner, &url, headers, ApiOperation::Pipeline)
45    }
46
47    fn lint(&self, _body: YamlBytes) -> Result<LintResponse> {
48        todo!()
49    }
50}
51
52impl<R> Github<R> {
53    fn resource_cicd_metadata_url(&self) -> (String, http::Headers) {
54        let url = format!(
55            "{}/repos/{}/actions/runs?page=1",
56            self.rest_api_basepath, self.path
57        );
58        let headers = self.request_headers();
59        (url, headers)
60    }
61}
62
63impl<R: HttpRunner<Response = HttpResponse>> CicdRunner for Github<R> {
64    fn list(&self, _args: RunnerListBodyArgs) -> Result<Vec<crate::cmds::cicd::Runner>> {
65        todo!();
66    }
67
68    fn get(&self, _id: i64) -> Result<RunnerMetadata> {
69        todo!();
70    }
71
72    fn num_pages(&self, _args: RunnerListBodyArgs) -> Result<Option<u32>> {
73        todo!();
74    }
75
76    fn num_resources(&self, _args: RunnerListBodyArgs) -> Result<Option<NumberDeltaErr>> {
77        todo!()
78    }
79
80    fn create(&self, _args: RunnerPostDataCliArgs) -> Result<RunnerRegistrationResponse> {
81        todo!()
82    }
83}
84
85impl<R: HttpRunner<Response = HttpResponse>> CicdJob for Github<R> {
86    fn list(&self, _args: JobListBodyArgs) -> Result<Vec<Job>> {
87        todo!();
88    }
89
90    fn num_pages(&self, _args: JobListBodyArgs) -> Result<Option<u32>> {
91        todo!();
92    }
93
94    fn num_resources(
95        &self,
96        _args: JobListBodyArgs,
97    ) -> Result<Option<crate::api_traits::NumberDeltaErr>> {
98        todo!();
99    }
100}
101
102pub struct GithubPipelineFields {
103    pipeline: Pipeline,
104}
105
106impl From<&serde_json::Value> for GithubPipelineFields {
107    fn from(pipeline_data: &serde_json::Value) -> Self {
108        GithubPipelineFields {
109            pipeline: Pipeline::builder()
110                .id(pipeline_data["id"].as_i64().unwrap_or_default())
111                // Github has `conclusion` as the final
112                // state of the pipeline. It also has a
113                // `status` field to represent the current
114                // state of the pipeline. Our domain
115                // `Pipeline` struct `status` refers to the
116                // final state, i.e conclusion.
117                .status(
118                    pipeline_data["conclusion"]
119                        .as_str()
120                        // conclusion is not present when a
121                        // pipeline is running, gather its status.
122                        .unwrap_or_else(||
123                            // set is as unknown if
124// neither conclusion nor status are present.
125                            pipeline_data["status"].as_str().unwrap_or("unknown"))
126                        .to_string(),
127                )
128                .web_url(pipeline_data["html_url"].as_str().unwrap().to_string())
129                .branch(pipeline_data["head_branch"].as_str().unwrap().to_string())
130                .sha(pipeline_data["head_sha"].as_str().unwrap().to_string())
131                .created_at(pipeline_data["created_at"].as_str().unwrap().to_string())
132                .updated_at(pipeline_data["updated_at"].as_str().unwrap().to_string())
133                .duration(time::compute_duration(
134                    pipeline_data["created_at"].as_str().unwrap(),
135                    pipeline_data["updated_at"].as_str().unwrap(),
136                ))
137                .build()
138                .unwrap(),
139        }
140    }
141}
142
143impl From<GithubPipelineFields> for Pipeline {
144    fn from(fields: GithubPipelineFields) -> Self {
145        fields.pipeline
146    }
147}
148
149#[cfg(test)]
150mod test {
151
152    use crate::{
153        error,
154        http::Headers,
155        remote::ListBodyArgs,
156        setup_client,
157        test::utils::{default_github, get_contract, ContractType, ResponseContracts},
158    };
159
160    use super::*;
161
162    #[test]
163    fn test_list_actions() {
164        let contracts = ResponseContracts::new(ContractType::Github).add_contract(
165            200,
166            "list_pipelines.json",
167            None,
168        );
169        let (client, github) = setup_client!(contracts, default_github(), dyn Cicd);
170        let args = PipelineBodyArgs::builder()
171            .from_to_page(None)
172            .build()
173            .unwrap();
174        let runs = github.list(args).unwrap();
175        assert_eq!(
176            "https://api.github.com/repos/jordilin/githapi/actions/runs",
177            *client.url(),
178        );
179        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
180        assert_eq!(1, runs.len());
181    }
182
183    #[test]
184    fn test_list_actions_error_status_code() {
185        let contracts =
186            ResponseContracts::new(ContractType::Github).add_body::<String>(401, None, None);
187        let (_, github) = setup_client!(contracts, default_github(), dyn Cicd);
188        let args = PipelineBodyArgs::builder()
189            .from_to_page(None)
190            .build()
191            .unwrap();
192        assert!(github.list(args).is_err());
193    }
194
195    #[test]
196    fn test_list_actions_unexpected_ok_status_code() {
197        let contracts =
198            ResponseContracts::new(ContractType::Github).add_body::<String>(302, None, None);
199        let (_, github) = setup_client!(contracts, default_github(), dyn Cicd);
200        let args = PipelineBodyArgs::builder()
201            .from_to_page(None)
202            .build()
203            .unwrap();
204        match github.list(args) {
205            Ok(_) => panic!("Expected error"),
206            Err(err) => match err.downcast_ref::<error::GRError>() {
207                Some(error::GRError::RemoteServerError(_)) => (),
208                _ => panic!("Expected error::GRError::RemoteServerError"),
209            },
210        }
211    }
212
213    #[test]
214    fn test_list_actions_empty_workflow_runs() {
215        let contracts = ResponseContracts::new(ContractType::Github).add_body::<String>(
216            200,
217            Some(r#"{"workflow_runs":[]}"#.to_string()),
218            None,
219        );
220        let (_, github) = setup_client!(contracts, default_github(), dyn Cicd);
221        let args = PipelineBodyArgs::builder()
222            .from_to_page(None)
223            .build()
224            .unwrap();
225        assert_eq!(0, github.list(args).unwrap().len());
226    }
227
228    #[test]
229    fn test_workflow_runs_not_an_array_is_error() {
230        let contracts = ResponseContracts::new(ContractType::Github).add_body::<String>(
231            200,
232            Some(r#"{"workflow_runs":{}}"#.to_string()),
233            None,
234        );
235        let (_, github) = setup_client!(contracts, default_github(), dyn Cicd);
236        let args = PipelineBodyArgs::builder()
237            .from_to_page(None)
238            .build()
239            .unwrap();
240        match github.list(args) {
241            Ok(_) => panic!("Expected error"),
242            Err(err) => match err.downcast_ref::<error::GRError>() {
243                Some(error::GRError::RemoteUnexpectedResponseContract(_)) => (),
244                _ => panic!("Expected error::GRError::RemoteUnexpectedResponseContract"),
245            },
246        }
247    }
248
249    #[test]
250    fn test_num_pages_for_list_actions() {
251        let link_header = r#"<https://api.github.com/repos/jordilin/githapi/actions/runs?page=1>; rel="next", <https://api.github.com/repos/jordilin/githapi/actions/runs?page=1>; rel="last""#;
252        let mut headers = Headers::new();
253        headers.set("link".to_string(), link_header.to_string());
254        let contracts = ResponseContracts::new(ContractType::Github).add_body::<String>(
255            200,
256            None,
257            Some(headers),
258        );
259        let (client, github) = setup_client!(contracts, default_github(), dyn Cicd);
260        assert_eq!(Some(1), github.num_pages().unwrap());
261        assert_eq!(
262            "https://api.github.com/repos/jordilin/githapi/actions/runs?page=1",
263            *client.url(),
264        );
265        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
266    }
267
268    #[test]
269    fn test_num_pages_error_retrieving_last_page() {
270        let contracts =
271            ResponseContracts::new(ContractType::Github).add_body::<String>(200, None, None);
272        let (_, github) = setup_client!(contracts, default_github(), dyn Cicd);
273        assert_eq!(Some(1), github.num_pages().unwrap());
274    }
275
276    #[test]
277    fn test_list_actions_from_page_set_in_url() {
278        let contracts = ResponseContracts::new(ContractType::Github).add_contract(
279            200,
280            "list_pipelines.json",
281            None,
282        );
283        let (client, github) = setup_client!(contracts, default_github(), dyn Cicd);
284        let args = PipelineBodyArgs::builder()
285            .from_to_page(Some(
286                ListBodyArgs::builder()
287                    .page(2)
288                    .max_pages(3)
289                    .build()
290                    .unwrap(),
291            ))
292            .build()
293            .unwrap();
294        github.list(args).unwrap();
295        assert_eq!(
296            "https://api.github.com/repos/jordilin/githapi/actions/runs?page=2",
297            *client.url(),
298        );
299        assert_eq!(Some(ApiOperation::Pipeline), *client.api_operation.borrow());
300    }
301
302    #[test]
303    fn test_list_actions_conclusion_field_not_available_use_status() {
304        let contract_json = get_contract(ContractType::Github, "list_pipelines.json");
305        let contract_json = contract_json
306            .lines()
307            .filter(|line| !line.contains("conclusion"))
308            .collect::<Vec<&str>>()
309            .join("\n");
310        let contracts = ResponseContracts::new(ContractType::Github).add_body::<String>(
311            200,
312            Some(contract_json),
313            None,
314        );
315        let (_, github) = setup_client!(contracts, default_github(), dyn Cicd);
316        let args = PipelineBodyArgs::builder()
317            .from_to_page(None)
318            .build()
319            .unwrap();
320        let runs = github.list(args).unwrap();
321        assert_eq!("completed", runs[0].status);
322    }
323
324    #[test]
325    fn test_list_actions_neither_conclusion_nor_status_use_unknown() {
326        let contract_json = get_contract(ContractType::Github, "list_pipelines.json");
327        let contract_json = contract_json
328            .lines()
329            .filter(|line| !line.contains("conclusion") && !line.contains("status"))
330            .collect::<Vec<&str>>()
331            .join("\n");
332        let contracts = ResponseContracts::new(ContractType::Github).add_body::<String>(
333            200,
334            Some(contract_json),
335            None,
336        );
337        let (_, github) = setup_client!(contracts, default_github(), dyn Cicd);
338        let args = PipelineBodyArgs::builder()
339            .from_to_page(None)
340            .build()
341            .unwrap();
342        let runs = github.list(args).unwrap();
343        assert_eq!("unknown", runs[0].status);
344    }
345}