Skip to main content

gitlab/api/projects/repository/commits/
create_status.rs

1// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
2// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
3// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
4// option. This file may not be copied, modified, or distributed
5// except according to those terms.
6
7use derive_builder::Builder;
8
9use crate::api::common::{self, NameOrId};
10use crate::api::endpoint_prelude::*;
11use crate::api::ParamValue;
12
13/// The state a commit status may have.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum CommitStatusState {
17    /// The status is expected, but has not started yet.
18    Pending,
19    /// The status is currently running.
20    Running,
21    /// The status completed successfully.
22    Success,
23    /// The status completed with an error.
24    Failed,
25    /// The status was canceled before it completed.
26    Canceled,
27    /// The status was skipped.
28    Skipped,
29}
30
31impl CommitStatusState {
32    fn as_str(self) -> &'static str {
33        match self {
34            CommitStatusState::Pending => "pending",
35            CommitStatusState::Running => "running",
36            CommitStatusState::Success => "success",
37            CommitStatusState::Failed => "failed",
38            CommitStatusState::Canceled => "canceled",
39            CommitStatusState::Skipped => "skipped",
40        }
41    }
42}
43
44impl ParamValue<'static> for CommitStatusState {
45    fn as_value(&self) -> Cow<'static, str> {
46        self.as_str().into()
47    }
48}
49
50/// Post a comment on a specific commit in a project.
51#[derive(Debug, Builder, Clone)]
52#[builder(setter(strip_option), build_fn(validate = "Self::validate_fields"))]
53pub struct CreateCommitStatus<'a> {
54    /// The project to get a commit from.
55    #[builder(setter(into))]
56    project: NameOrId<'a>,
57    /// The commit to comment on.
58    #[builder(setter(into))]
59    commit: Cow<'a, str>,
60    /// The state of the commit status.
61    #[builder(setter(into))]
62    state: CommitStatusState,
63
64    /// The name of the status.
65    #[builder(setter(into), default)]
66    name: Option<Cow<'a, str>>,
67    /// The name of the ref for the commit.
68    ///
69    /// Must be 255 characters or fewer.
70    #[builder(setter(into), default)]
71    ref_: Option<Cow<'a, str>>,
72    /// The URL to use for more details.
73    ///
74    /// Must be 255 characters or fewer.
75    #[builder(setter(into), default)]
76    target_url: Option<Cow<'a, str>>,
77    /// A description for the status.
78    ///
79    /// Must be 255 characters or fewer.
80    #[builder(setter(into), default)]
81    description: Option<Cow<'a, str>>,
82    /// The total code coverage (as a percentage).
83    #[builder(default)]
84    coverage: Option<f64>,
85    /// The ID of the pipeline to use (in case it is ambiguous).
86    #[builder(default)]
87    pipeline_id: Option<u64>,
88}
89
90impl<'a> CreateCommitStatus<'a> {
91    /// Create a builder for the endpoint.
92    pub fn builder() -> CreateCommitStatusBuilder<'a> {
93        CreateCommitStatusBuilder::default()
94    }
95}
96
97impl CreateCommitStatusBuilder<'_> {
98    const MAX_FIELD_LENGTH: usize = 255;
99
100    fn validate_fields(&self) -> Result<(), String> {
101        if let Some(Some(ref r)) = self.ref_ {
102            if r.len() > Self::MAX_FIELD_LENGTH {
103                return Err(format!(
104                    "ref_ exceeds maximum length of {} characters",
105                    Self::MAX_FIELD_LENGTH,
106                ));
107            }
108        }
109        if let Some(Some(ref url)) = self.target_url {
110            if url.len() > Self::MAX_FIELD_LENGTH {
111                return Err(format!(
112                    "target_url exceeds maximum length of {} characters",
113                    Self::MAX_FIELD_LENGTH,
114                ));
115            }
116        }
117        if let Some(Some(ref desc)) = self.description {
118            if desc.len() > Self::MAX_FIELD_LENGTH {
119                return Err(format!(
120                    "description exceeds maximum length of {} characters",
121                    Self::MAX_FIELD_LENGTH,
122                ));
123            }
124        }
125        Ok(())
126    }
127}
128
129impl Endpoint for CreateCommitStatus<'_> {
130    fn method(&self) -> Method {
131        Method::POST
132    }
133
134    fn endpoint(&self) -> Cow<'static, str> {
135        format!(
136            // XXX(gitlab): This is a really bad name for this endpoint. It's the only one that
137            // exists in this namespace.
138            // https://gitlab.com/gitlab-org/gitlab/-/issues/217412
139            "projects/{}/statuses/{}",
140            self.project,
141            common::path_escaped(&self.commit),
142        )
143        .into()
144    }
145
146    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
147        let mut params = FormParams::default();
148
149        params
150            .push("state", self.state)
151            .push_opt("name", self.name.as_ref())
152            .push_opt("ref", self.ref_.as_ref())
153            .push_opt("target_url", self.target_url.as_ref())
154            .push_opt("description", self.description.as_ref())
155            .push_opt("coverage", self.coverage)
156            .push_opt("pipeline_id", self.pipeline_id);
157
158        params.into_body()
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use http::Method;
165
166    use crate::api::projects::repository::commits::{
167        CommitStatusState, CreateCommitStatus, CreateCommitStatusBuilderError,
168    };
169    use crate::api::{self, Query};
170    use crate::test::client::{ExpectedUrl, SingleTestClient};
171
172    #[test]
173    fn commit_status_state_as_str() {
174        let items = &[
175            (CommitStatusState::Pending, "pending"),
176            (CommitStatusState::Running, "running"),
177            (CommitStatusState::Success, "success"),
178            (CommitStatusState::Failed, "failed"),
179            (CommitStatusState::Canceled, "canceled"),
180            (CommitStatusState::Skipped, "skipped"),
181        ];
182
183        for (i, s) in items {
184            assert_eq!(i.as_str(), *s);
185        }
186    }
187
188    #[test]
189    fn project_commit_and_state_are_necessary() {
190        let err = CreateCommitStatus::builder().build().unwrap_err();
191        crate::test::assert_missing_field!(err, CreateCommitStatusBuilderError, "project");
192    }
193
194    #[test]
195    fn project_is_necessary() {
196        let err = CreateCommitStatus::builder()
197            .commit("master")
198            .state(CommitStatusState::Pending)
199            .build()
200            .unwrap_err();
201        crate::test::assert_missing_field!(err, CreateCommitStatusBuilderError, "project");
202    }
203
204    #[test]
205    fn commit_is_necessary() {
206        let err = CreateCommitStatus::builder()
207            .project(1)
208            .state(CommitStatusState::Pending)
209            .build()
210            .unwrap_err();
211        crate::test::assert_missing_field!(err, CreateCommitStatusBuilderError, "commit");
212    }
213
214    #[test]
215    fn state_is_necessary() {
216        let err = CreateCommitStatus::builder()
217            .project(1)
218            .commit("master")
219            .build()
220            .unwrap_err();
221        crate::test::assert_missing_field!(err, CreateCommitStatusBuilderError, "state");
222    }
223
224    #[test]
225    fn project_commit_and_state_are_sufficient() {
226        CreateCommitStatus::builder()
227            .project(1)
228            .commit("master")
229            .state(CommitStatusState::Pending)
230            .build()
231            .unwrap();
232    }
233
234    #[test]
235    fn endpoint() {
236        let endpoint = ExpectedUrl::builder()
237            .method(Method::POST)
238            .endpoint("projects/simple%2Fproject/statuses/0000000000000000000000000000000000000000")
239            .content_type("application/x-www-form-urlencoded")
240            .body_str("state=pending")
241            .build()
242            .unwrap();
243        let client = SingleTestClient::new_raw(endpoint, "");
244
245        let endpoint = CreateCommitStatus::builder()
246            .project("simple/project")
247            .commit("0000000000000000000000000000000000000000")
248            .state(CommitStatusState::Pending)
249            .build()
250            .unwrap();
251        api::ignore(endpoint).query(&client).unwrap();
252    }
253
254    #[test]
255    fn endpoint_name() {
256        let endpoint = ExpectedUrl::builder()
257            .method(Method::POST)
258            .endpoint("projects/simple%2Fproject/statuses/0000000000000000000000000000000000000000")
259            .content_type("application/x-www-form-urlencoded")
260            .body_str(concat!("state=pending", "&name=jobname"))
261            .build()
262            .unwrap();
263        let client = SingleTestClient::new_raw(endpoint, "");
264
265        let endpoint = CreateCommitStatus::builder()
266            .project("simple/project")
267            .commit("0000000000000000000000000000000000000000")
268            .state(CommitStatusState::Pending)
269            .name("jobname")
270            .build()
271            .unwrap();
272        api::ignore(endpoint).query(&client).unwrap();
273    }
274
275    #[test]
276    fn endpoint_ref() {
277        let endpoint = ExpectedUrl::builder()
278            .method(Method::POST)
279            .endpoint("projects/simple%2Fproject/statuses/0000000000000000000000000000000000000000")
280            .content_type("application/x-www-form-urlencoded")
281            .body_str(concat!("state=pending", "&ref=refname"))
282            .build()
283            .unwrap();
284        let client = SingleTestClient::new_raw(endpoint, "");
285
286        let endpoint = CreateCommitStatus::builder()
287            .project("simple/project")
288            .commit("0000000000000000000000000000000000000000")
289            .state(CommitStatusState::Pending)
290            .ref_("refname")
291            .build()
292            .unwrap();
293        api::ignore(endpoint).query(&client).unwrap();
294    }
295
296    #[test]
297    fn endpoint_target_url() {
298        let endpoint = ExpectedUrl::builder()
299            .method(Method::POST)
300            .endpoint("projects/simple%2Fproject/statuses/0000000000000000000000000000000000000000")
301            .content_type("application/x-www-form-urlencoded")
302            .body_str(concat!(
303                "state=pending",
304                "&target_url=https%3A%2F%2Ftest.invalid%2Fpath%3Fsome%3Dfoo",
305            ))
306            .build()
307            .unwrap();
308        let client = SingleTestClient::new_raw(endpoint, "");
309
310        let endpoint = CreateCommitStatus::builder()
311            .project("simple/project")
312            .commit("0000000000000000000000000000000000000000")
313            .state(CommitStatusState::Pending)
314            .target_url("https://test.invalid/path?some=foo")
315            .build()
316            .unwrap();
317        api::ignore(endpoint).query(&client).unwrap();
318    }
319
320    #[test]
321    fn endpoint_description() {
322        let endpoint = ExpectedUrl::builder()
323            .method(Method::POST)
324            .endpoint("projects/simple%2Fproject/statuses/0000000000000000000000000000000000000000")
325            .content_type("application/x-www-form-urlencoded")
326            .body_str(concat!("state=pending", "&description=description"))
327            .build()
328            .unwrap();
329        let client = SingleTestClient::new_raw(endpoint, "");
330
331        let endpoint = CreateCommitStatus::builder()
332            .project("simple/project")
333            .commit("0000000000000000000000000000000000000000")
334            .state(CommitStatusState::Pending)
335            .description("description")
336            .build()
337            .unwrap();
338        api::ignore(endpoint).query(&client).unwrap();
339    }
340
341    #[test]
342    fn endpoint_coverage() {
343        let endpoint = ExpectedUrl::builder()
344            .method(Method::POST)
345            .endpoint("projects/simple%2Fproject/statuses/0000000000000000000000000000000000000000")
346            .content_type("application/x-www-form-urlencoded")
347            .body_str(concat!("state=pending", "&coverage=90"))
348            .build()
349            .unwrap();
350        let client = SingleTestClient::new_raw(endpoint, "");
351
352        let endpoint = CreateCommitStatus::builder()
353            .project("simple/project")
354            .commit("0000000000000000000000000000000000000000")
355            .state(CommitStatusState::Pending)
356            .coverage(90.)
357            .build()
358            .unwrap();
359        api::ignore(endpoint).query(&client).unwrap();
360    }
361
362    #[test]
363    fn endpoint_pipeline_id() {
364        let endpoint = ExpectedUrl::builder()
365            .method(Method::POST)
366            .endpoint("projects/simple%2Fproject/statuses/0000000000000000000000000000000000000000")
367            .content_type("application/x-www-form-urlencoded")
368            .body_str(concat!("state=pending", "&pipeline_id=1"))
369            .build()
370            .unwrap();
371        let client = SingleTestClient::new_raw(endpoint, "");
372
373        let endpoint = CreateCommitStatus::builder()
374            .project("simple/project")
375            .commit("0000000000000000000000000000000000000000")
376            .state(CommitStatusState::Pending)
377            .pipeline_id(1)
378            .build()
379            .unwrap();
380        api::ignore(endpoint).query(&client).unwrap();
381    }
382
383    #[test]
384    fn endpoint_field_length_validation() {
385        const MAX_LEN: usize = 255;
386        let long_string: String = "a".repeat(MAX_LEN + 1);
387        let valid_string: String = "a".repeat(MAX_LEN);
388
389        // Test ref_
390        let err = CreateCommitStatus::builder()
391            .project(1)
392            .commit("master")
393            .state(CommitStatusState::Pending)
394            .ref_(&long_string)
395            .build()
396            .unwrap_err();
397        assert!(matches!(
398            err,
399            CreateCommitStatusBuilderError::ValidationError(_),
400        ));
401        if let CreateCommitStatusBuilderError::ValidationError(msg) = err {
402            assert!(msg.contains("ref_ exceeds maximum length"));
403        }
404
405        let status = CreateCommitStatus::builder()
406            .project(1)
407            .commit("master")
408            .state(CommitStatusState::Pending)
409            .ref_(&valid_string)
410            .build()
411            .unwrap();
412        assert_eq!(status.ref_.as_ref().unwrap(), &valid_string);
413
414        // Test target_url
415        let err = CreateCommitStatus::builder()
416            .project(1)
417            .commit("master")
418            .state(CommitStatusState::Pending)
419            .target_url(&long_string)
420            .build()
421            .unwrap_err();
422        assert!(matches!(
423            err,
424            CreateCommitStatusBuilderError::ValidationError(_),
425        ));
426        if let CreateCommitStatusBuilderError::ValidationError(msg) = err {
427            assert!(msg.contains("target_url exceeds maximum length"));
428        }
429
430        let status = CreateCommitStatus::builder()
431            .project(1)
432            .commit("master")
433            .state(CommitStatusState::Pending)
434            .target_url(&valid_string)
435            .build()
436            .unwrap();
437        assert_eq!(status.target_url.as_ref().unwrap(), &valid_string);
438
439        // Test description
440        let err = CreateCommitStatus::builder()
441            .project(1)
442            .commit("master")
443            .state(CommitStatusState::Pending)
444            .description(&long_string)
445            .build()
446            .unwrap_err();
447        assert!(matches!(
448            err,
449            CreateCommitStatusBuilderError::ValidationError(_),
450        ));
451        if let CreateCommitStatusBuilderError::ValidationError(msg) = err {
452            assert!(msg.contains("description exceeds maximum length"));
453        }
454
455        let status = CreateCommitStatus::builder()
456            .project(1)
457            .commit("master")
458            .state(CommitStatusState::Pending)
459            .description(&valid_string)
460            .build()
461            .unwrap();
462        assert_eq!(status.description.as_ref().unwrap(), &valid_string);
463    }
464}