superkeyloader_lib/
github.rs1extern crate pretty_env_logger;
2
3use regex::RegexSet;
4
5pub const INVALID_GH_USERNAME: u16 = 1001;
6pub const INVALID_GH_API_RESPONSE: u16 = 1002;
7
8#[derive(Debug, Serialize, Deserialize)]
40pub struct GhKey {
41 pub id: u64,
42 pub key: String,
43}
44
45fn validate_username(username: &str) -> bool {
60 let username_rules = RegexSet::new(vec![
61 r"^([-a-zA-Z\d]){1,39}$",
62 r".*[^-]$",
63 r"^([^-]+|-($[^-]))*$",
64 ])
65 .unwrap();
66
67 let matches: Vec<_> = username_rules.matches(username).into_iter().collect();
68 username_rules.len() == matches.len()
70}
71
72pub fn get_keys(username: &str, token: Option<String>) -> Result<Vec<String>, u16> {
106 if !validate_username(username) {
107 return Err(INVALID_GH_USERNAME);
108 }
109
110 #[cfg(not(test))]
112 let gh_api_url: &str = "https://api.github.com";
113 #[cfg(test)]
114 let gh_api_url: &str = &mockito::server_url();
115 debug!("GitHub API base URL: {}", gh_api_url);
116
117 let url = format!("{}/users/{}/keys", gh_api_url, username);
120 debug!("GitHub API endpoint URL: {}", url);
121
122 let mut request = ureq::get(&url);
123
124 if let Some(oauth_token) = token {
125 request.set("Authorization", format!("token {}", oauth_token).as_ref());
126 }
127
128 let response = request.call();
129
130 if !response.ok() {
131 return Err(response.status());
132 }
133
134 let resp_json = response.into_string().unwrap();
135 let parsed_json = serde_json::from_str(&resp_json);
136
137 if parsed_json.is_err() {
138 return Err(INVALID_GH_API_RESPONSE);
139 }
140
141 let gh_keys: Vec<GhKey> = parsed_json.unwrap();
142
143 let keys = gh_keys
144 .into_iter()
145 .map(|key| format!("{} from-GH-id-{}", key.key, key.id))
146 .collect();
147
148 Ok(keys)
149}
150
151pub mod test_values {
152
153 pub const VALID_USERNAME: &str = "testuser";
154 pub const MISSING_USERNAME: &str = "erruser";
155 pub const INVALID_USERNAME_LENGTH: &str = "user-user-user-user-user-user-user-user-";
156 pub const INVALID_USERNAME_ENDING_HYPHEN: &str = "user-user-";
157 pub const INVALID_USERNAME_CONSEC_HYPHEN: &str = "user--user";
158
159 pub const VALID_3_KEYS_JSON: &str = r#"[
160 {
161 "id": 12257919,
162 "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCarT/me5sWxY9Tizc"
163 },
164 {
165 "id": 22932337,
166 "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC+MxvBji8iUuN2so2"
167 },
168 {
169 "id": 69196823,
170 "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDq/BrJT0c7LSmTRDE"
171 }
172 ]"#;
173
174 pub const EMPTY_JSON: &str = r#"[]"#;
175
176 pub const INVALID_JSON: &str = r#"[
177 {
178 "id": "12257919",
179 "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCarT/me5sWxY9Tizc"
180 },
181 {
182 "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC+MxvBji8iUuN2so2"
183 },
184 {
185 "id": 69196823,
186 "key": 42
187 }
188 ]"#;
189}
190
191#[cfg(test)]
192mod tests {
193
194 use super::test_values::*;
195
196 use mockito::mock;
197
198 #[test]
199 fn test_github_username_validation() {
200 assert_eq!(
201 super::validate_username(&String::from(VALID_USERNAME)),
202 true
203 );
204 assert_eq!(
205 super::validate_username(&String::from(INVALID_USERNAME_LENGTH)),
206 false
207 );
208 assert_eq!(
209 super::validate_username(&String::from(INVALID_USERNAME_ENDING_HYPHEN)),
210 false
211 );
212 assert_eq!(
213 super::validate_username(&String::from(INVALID_USERNAME_CONSEC_HYPHEN)),
214 false
215 );
216 }
217
218 #[test]
219 fn valid_response() {
220 let _m = mock("GET", "/users/testuser/keys")
221 .with_status(200)
222 .with_header("Content-Type", "application/json; charset=utf-8")
223 .with_body(VALID_3_KEYS_JSON)
224 .create();
225
226 let result = super::get_keys(&String::from(VALID_USERNAME), None);
227
228 assert_eq!(result.is_ok(), true);
229 assert_eq!(result.unwrap().len(), 3);
230 }
231
232 #[test]
233 fn invalid_response() {
234 let _m = mock("GET", "/users/testuser/keys")
235 .with_status(200)
236 .with_header("Content-Type", "application/json; charset=utf-8")
237 .with_body(INVALID_JSON)
238 .create();
239
240 let result = super::get_keys(&String::from(VALID_USERNAME), None);
241
242 assert_eq!(result.is_ok(), false);
243 assert_eq!(result.err().unwrap(), super::INVALID_GH_API_RESPONSE);
244 }
245
246 #[test]
247 fn no_keys_response() {
248 let _m = mock("GET", "/users/testuser/keys")
249 .with_status(200)
250 .with_header("Content-Type", "application/json; charset=utf-8")
251 .with_body(EMPTY_JSON)
252 .create();
253
254 let result = super::get_keys(&String::from(VALID_USERNAME), None);
255
256 assert_eq!(result.is_ok(), true);
257 assert_eq!(result.unwrap().len(), 0);
258 }
259
260 #[test]
261 fn missing_username() {
262 let _m = mock("GET", "/users/erruser/keys")
263 .with_status(404)
264 .with_header("Content-Type", "application/json; charset=utf-8")
265 .with_body(VALID_3_KEYS_JSON)
266 .create();
267
268 let result = super::get_keys(&String::from(MISSING_USERNAME), None);
269
270 assert_eq!(result.is_ok(), false);
271 assert_eq!(result.err().unwrap(), 404);
272 }
273
274 #[test]
275 fn invalid_username() {
276 let _m = mock("GET", "/users/testuser/keys")
277 .with_status(200)
278 .with_header("Content-Type", "application/json; charset=utf-8")
279 .with_body(VALID_3_KEYS_JSON)
280 .create();
281
282 let result = super::get_keys(&String::from(INVALID_USERNAME_LENGTH), None);
284 assert_eq!(result.is_ok(), false);
285 assert_eq!(result.err().unwrap(), super::INVALID_GH_USERNAME);
286
287 let result = super::get_keys(&String::from(INVALID_USERNAME_ENDING_HYPHEN), None);
289 assert_eq!(result.is_ok(), false);
290 assert_eq!(result.err().unwrap(), super::INVALID_GH_USERNAME);
291
292 let result = super::get_keys(&String::from(INVALID_USERNAME_CONSEC_HYPHEN), None);
294 assert_eq!(result.is_ok(), false);
295 assert_eq!(result.err().unwrap(), super::INVALID_GH_USERNAME);
296 }
297}