extern crate pretty_env_logger;
use regex::RegexSet;
pub const INVALID_GH_USERNAME: u16 = 1001;
pub const INVALID_GH_API_RESPONSE: u16 = 1002;
#[derive(Debug, Serialize, Deserialize)]
pub struct GhKey {
pub id: u64,
pub key: String,
}
fn validate_username(username: &str) -> bool {
let username_rules = RegexSet::new(vec![
r"^([-a-zA-Z\d]){1,39}$",
r".*[^-]$",
r"^([^-]+|-($[^-]))*$",
])
.unwrap();
let matches: Vec<_> = username_rules.matches(username).into_iter().collect();
username_rules.len() == matches.len()
}
pub fn get_keys(username: &str, token: Option<String>) -> Result<Vec<String>, u16> {
if !validate_username(username) {
return Err(INVALID_GH_USERNAME);
}
#[cfg(not(test))]
let gh_api_url: &str = "https://api.github.com";
#[cfg(test)]
let gh_api_url: &str = &mockito::server_url();
debug!("GitHub API base URL: {}", gh_api_url);
let url = format!("{}/users/{}/keys", gh_api_url, username);
debug!("GitHub API endpoint URL: {}", url);
let mut request = ureq::get(&url);
if let Some(oauth_token) = token {
request.set("Authorization", format!("token {}", oauth_token).as_ref());
}
let response = request.call();
if !response.ok() {
return Err(response.status());
}
let resp_json = response.into_string().unwrap();
let parsed_json = serde_json::from_str(&resp_json);
if parsed_json.is_err() {
return Err(INVALID_GH_API_RESPONSE);
}
let gh_keys: Vec<GhKey> = parsed_json.unwrap();
let keys = gh_keys
.into_iter()
.map(|key| format!("{} from-GH-id-{}", key.key, key.id))
.collect();
Ok(keys)
}
pub mod test_values {
pub const VALID_USERNAME: &str = "testuser";
pub const MISSING_USERNAME: &str = "erruser";
pub const INVALID_USERNAME_LENGTH: &str = "user-user-user-user-user-user-user-user-";
pub const INVALID_USERNAME_ENDING_HYPHEN: &str = "user-user-";
pub const INVALID_USERNAME_CONSEC_HYPHEN: &str = "user--user";
pub const VALID_3_KEYS_JSON: &str = r#"[
{
"id": 12257919,
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCarT/me5sWxY9Tizc"
},
{
"id": 22932337,
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC+MxvBji8iUuN2so2"
},
{
"id": 69196823,
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDq/BrJT0c7LSmTRDE"
}
]"#;
pub const EMPTY_JSON: &str = r#"[]"#;
pub const INVALID_JSON: &str = r#"[
{
"id": "12257919",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCarT/me5sWxY9Tizc"
},
{
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC+MxvBji8iUuN2so2"
},
{
"id": 69196823,
"key": 42
}
]"#;
}
#[cfg(test)]
mod tests {
use super::test_values::*;
use mockito::mock;
#[test]
fn test_github_username_validation() {
assert_eq!(
super::validate_username(&String::from(VALID_USERNAME)),
true
);
assert_eq!(
super::validate_username(&String::from(INVALID_USERNAME_LENGTH)),
false
);
assert_eq!(
super::validate_username(&String::from(INVALID_USERNAME_ENDING_HYPHEN)),
false
);
assert_eq!(
super::validate_username(&String::from(INVALID_USERNAME_CONSEC_HYPHEN)),
false
);
}
#[test]
fn valid_response() {
let _m = mock("GET", "/users/testuser/keys")
.with_status(200)
.with_header("Content-Type", "application/json; charset=utf-8")
.with_body(VALID_3_KEYS_JSON)
.create();
let result = super::get_keys(&String::from(VALID_USERNAME), None);
assert_eq!(result.is_ok(), true);
assert_eq!(result.unwrap().len(), 3);
}
#[test]
fn invalid_response() {
let _m = mock("GET", "/users/testuser/keys")
.with_status(200)
.with_header("Content-Type", "application/json; charset=utf-8")
.with_body(INVALID_JSON)
.create();
let result = super::get_keys(&String::from(VALID_USERNAME), None);
assert_eq!(result.is_ok(), false);
assert_eq!(result.err().unwrap(), super::INVALID_GH_API_RESPONSE);
}
#[test]
fn no_keys_response() {
let _m = mock("GET", "/users/testuser/keys")
.with_status(200)
.with_header("Content-Type", "application/json; charset=utf-8")
.with_body(EMPTY_JSON)
.create();
let result = super::get_keys(&String::from(VALID_USERNAME), None);
assert_eq!(result.is_ok(), true);
assert_eq!(result.unwrap().len(), 0);
}
#[test]
fn missing_username() {
let _m = mock("GET", "/users/erruser/keys")
.with_status(404)
.with_header("Content-Type", "application/json; charset=utf-8")
.with_body(VALID_3_KEYS_JSON)
.create();
let result = super::get_keys(&String::from(MISSING_USERNAME), None);
assert_eq!(result.is_ok(), false);
assert_eq!(result.err().unwrap(), 404);
}
#[test]
fn invalid_username() {
let _m = mock("GET", "/users/testuser/keys")
.with_status(200)
.with_header("Content-Type", "application/json; charset=utf-8")
.with_body(VALID_3_KEYS_JSON)
.create();
let result = super::get_keys(&String::from(INVALID_USERNAME_LENGTH), None);
assert_eq!(result.is_ok(), false);
assert_eq!(result.err().unwrap(), super::INVALID_GH_USERNAME);
let result = super::get_keys(&String::from(INVALID_USERNAME_ENDING_HYPHEN), None);
assert_eq!(result.is_ok(), false);
assert_eq!(result.err().unwrap(), super::INVALID_GH_USERNAME);
let result = super::get_keys(&String::from(INVALID_USERNAME_CONSEC_HYPHEN), None);
assert_eq!(result.is_ok(), false);
assert_eq!(result.err().unwrap(), super::INVALID_GH_USERNAME);
}
}