1use crate::{DetectionState, DetectionStrategy};
4
5#[derive(Debug, thiserror::Error)]
7pub enum Error {
8 #[error("ID token variable not found: {0}")]
10 Missing(String),
11}
12
13pub(crate) struct GitLabCI;
14
15impl GitLabCI {
16 fn normalized_audience(audience: &str) -> String {
25 audience
26 .chars()
27 .map(|c| {
28 if c.is_ascii_alphanumeric() {
29 c.to_ascii_uppercase()
30 } else {
31 '_'
32 }
33 })
34 .collect()
35 }
36}
37
38impl DetectionStrategy for GitLabCI {
39 type Error = Error;
40
41 fn new(_state: &DetectionState) -> Option<Self> {
42 std::env::var("GITLAB_CI")
43 .ok()
44 .filter(|v| v == "true")
47 .map(|_| GitLabCI)
48 }
49
50 async fn detect(&self, audience: &str) -> Result<crate::IdToken, Self::Error> {
58 let normalized_audience = Self::normalized_audience(audience);
59
60 let var_name = format!("{normalized_audience}_ID_TOKEN");
61 let token = std::env::var(&var_name).map_err(|_| Error::Missing(var_name))?;
62
63 Ok(crate::IdToken(token.into()))
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use crate::{DetectionStrategy as _, gitlab::Error, tests::EnvScope};
70
71 use super::GitLabCI;
72
73 #[test]
74 fn test_normalized_audience() {
75 let cases = [
76 ("sigstore", "SIGSTORE"),
77 ("http://test.audience", "HTTP___TEST_AUDIENCE"),
78 ("my-audience_123", "MY_AUDIENCE_123"),
79 ("Audience With Spaces!", "AUDIENCE_WITH_SPACES_"),
80 ("ðŸ˜", "_"),
84 ("ðŸ˜ðŸ˜ðŸ˜", "___"),
85 ];
86
87 for (input, expected) in cases {
88 assert_eq!(GitLabCI::normalized_audience(input), expected);
89 }
90 }
91
92 #[tokio::test]
93 async fn test_detected() {
94 let mut scope = EnvScope::new();
95 scope.setenv("GITLAB_CI", "true");
96
97 assert!(GitLabCI::new(&Default::default()).is_some())
98 }
99
100 #[tokio::test]
101 async fn test_not_detected() {
102 let mut scope = EnvScope::new();
103 scope.unsetenv("GITLAB_CI");
104
105 assert!(GitLabCI::new(&Default::default()).is_none());
106 }
107
108 #[tokio::test]
109 async fn test_not_detected_wrong_value() {
110 for value in &["", "false", "TRUE", "1", "yes"] {
111 let mut scope = EnvScope::new();
112 scope.setenv("GITLAB_CI", value);
113
114 assert!(GitLabCI::new(&Default::default()).is_none());
115 }
116 }
117
118 #[tokio::test]
119 async fn test_invalid_missing() {
120 let mut scope = EnvScope::new();
121 scope.setenv("GITLAB_CI", "true");
122 scope.setenv("WRONG_ID_TOKEN", "sometoken");
123
124 let detector = GitLabCI::new(&Default::default()).expect("should detect GitLab CI");
125 assert!(matches!(
126 detector.detect("bupkis").await,
127 Err(Error::Missing(_))
128 ));
129 }
130
131 #[tokio::test]
132 async fn test_ok() {
133 let mut scope = EnvScope::new();
134 scope.setenv("GITLAB_CI", "true");
135 scope.setenv("BUPKIS_ID_TOKEN", "sometoken");
136
137 let detector = GitLabCI::new(&Default::default()).expect("should detect GitLab CI");
138 let token = detector.detect("bupkis").await.expect("should fetch token");
139 assert_eq!(token.reveal(), "sometoken");
140 }
141}