diff --git a/src/api/users.rs b/src/api/users.rs index d6e0769c..f5b38dbf 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -12,6 +12,7 @@ use crate::api::users::user_emails::UserEmailsOpsBuilder; use crate::api::users::user_git_ssh_keys::UserGitSshKeysOpsBuilder; use crate::api::users::user_gpg_keys::UserGpgKeysOpsBuilder; use crate::api::users::user_social_accounts::UserSocialAccountsOpsBuilder; +use crate::api::users::user_ssh_signing_keys::UserSshSigningKeysOpsBuilder; use crate::models::UserId; use crate::params::users::emails::EmailVisibilityState; use crate::{error, GitHubError, Octocrab}; @@ -23,6 +24,7 @@ mod user_git_ssh_keys; mod user_gpg_keys; mod user_repos; mod user_social_accounts; +mod user_ssh_signing_keys; pub(crate) enum UserRef { ByString(String), @@ -212,4 +214,13 @@ impl<'octo> UserHandler<'octo> { pub fn social_accounts(&self) -> UserSocialAccountsOpsBuilder<'_, '_> { UserSocialAccountsOpsBuilder::new(self) } + + ///SSH signing key administration + ///* List SSH signing keys for the authenticated user + ///* Create an SSH signing key for the authenticated user + ///* Get an SSH signing key for the authenticated user + ///* Delete an SSH signing key for the authenticated user + pub fn ssh_signing_keys(&self) -> UserSshSigningKeysOpsBuilder<'_, '_> { + UserSshSigningKeysOpsBuilder::new(self) + } } diff --git a/src/api/users/user_ssh_signing_keys.rs b/src/api/users/user_ssh_signing_keys.rs new file mode 100644 index 00000000..ca7c04e2 --- /dev/null +++ b/src/api/users/user_ssh_signing_keys.rs @@ -0,0 +1,152 @@ +use crate::api::users::UserHandler; +use crate::models::SshSigningKey; +use crate::{FromResponse, Page}; + +#[derive(serde::Serialize)] +pub struct UserSshSigningKeysOpsBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b UserHandler<'octo>, + #[serde(skip_serializing_if = "Option::is_none")] + per_page: Option<u8>, + #[serde(skip_serializing_if = "Option::is_none")] + page: Option<u32>, +} + +impl<'octo, 'b> UserSshSigningKeysOpsBuilder<'octo, 'b> { + pub(crate) fn new(handler: &'b UserHandler<'octo>) -> Self { + Self { + handler, + per_page: None, + page: None, + } + } + + /// Results per page (max 100). + pub fn per_page(mut self, per_page: impl Into<u8>) -> Self { + self.per_page = Some(per_page.into()); + self + } + + /// Page number of the results to fetch. + pub fn page(mut self, page: impl Into<u32>) -> Self { + self.page = Some(page.into()); + self + } + + ///## List SSH signing keys for the authenticated user + ///works with the following fine-grained token types: + ///[GitHub App user access tokens](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app) + ///[Fine-grained personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) + /// + ///OAuth app tokens and personal access tokens (classic) need the `read:ssh_signing_key` scope + /// + ///The fine-grained token must have the following permission set: + ///* "SSH signing keys" user permissions (read) + /// + ///```no_run + /// use octocrab::models::SshSigningKey; + /// use octocrab::{Page, Result}; + /// async fn run() -> Result<Page<SshSigningKey>> { + /// octocrab::instance() + /// .users("current_user") + /// .ssh_signing_keys() + /// .per_page(42).page(3u32) + /// .list() + /// .await + /// } + pub async fn list(&self) -> crate::Result<Page<crate::models::SshSigningKey>> { + let route = "/user/ssh_signing_keys".to_string(); + self.handler.crab.get(route, Some(&self)).await + } + + ///## Get extended details for an SSH signing key for the authenticated user + /// + ///OAuth app tokens and personal access tokens (classic) need the `read:ssh_signing_key` scope to use this method. + /// + ///works with the following token types: + ///[GitHub App user access tokens](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app) + ///[Fine-grained personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) + /// + ///The fine-grained token must have the following permission set: + ///* "SSH signing keys" user permissions (read) + /// + ///```no_run + /// use octocrab::models::SshSigningKey; + /// use octocrab::Result; + /// async fn run() -> Result<SshSigningKey> { + /// octocrab::instance() + /// .users("current_user") + /// .ssh_signing_keys() + /// .get(42) + /// .await + /// } + pub async fn get(&self, ssh_signing_key_id: u64) -> crate::Result<SshSigningKey> { + let route = format!("/user/ssh_signing_keys/{ssh_signing_key_id}"); + self.handler.crab.get(route, None::<&()>).await + } + + ///## Create a SSH signing key for the authenticated user + /// OAuth app tokens and personal access tokens (classic) need the `write:ssh_signing_key` scope to use this method. + /// + ///works with the following fine-grained token types: + ///[GitHub App user access tokens](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app) + ///[Fine-grained personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) + /// + ///The fine-grained token must have the following permission set: + ///* "SSH signing keys" user permissions (write) + /// + ///```no_run + /// use octocrab::models::SshSigningKey; + /// use octocrab::Result; + /// async fn run() -> Result<SshSigningKey> { + /// octocrab::instance() + /// .users("current_user") + /// .ssh_signing_keys() + /// .add("ssh-rsa AAAAB3NzaC1yc2EAAA".to_string(), "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234".to_string()) + /// .await + /// } + pub async fn add(&self, title: String, key: String) -> crate::Result<SshSigningKey> { + let route = "/user/ssh_signing_keys".to_string(); + + let params = serde_json::json!({ + "title": title, + "key": key, + }); + let response = self.handler.crab._post(route, Some(¶ms)).await?; + if response.status() != http::StatusCode::CREATED { + return Err(crate::map_github_error(response).await.unwrap_err()); + } + + <SshSigningKey>::from_response(crate::map_github_error(response).await?).await + } + + ///## Delete an SSH signing key for the authenticated user + /// OAuth app tokens and personal access tokens (classic) need the `admin:ssh_signing_key` scope + /// + ///works with the following fine-grained token types: + ///[GitHub App user access tokens](https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-user-access-token-for-a-github-app) + ///[Fine-grained personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) + /// + ///The fine-grained token must have the following permission set: + ///* "SSH signing keys" user permissions (write) + /// + ///```no_run + /// use octocrab::Result; + /// async fn run() -> Result<()> { + /// octocrab::instance() + /// .users("current_user") + /// .ssh_signing_keys() + /// .delete(42) + /// .await + /// } + pub async fn delete(&self, ssh_signing_key_id: u64) -> crate::Result<()> { + let route = format!("/user/ssh_signing_keys/{ssh_signing_key_id}"); + + let response = self.handler.crab._delete(route, None::<&()>).await?; + if response.status() != http::StatusCode::NO_CONTENT { + return Err(crate::map_github_error(response).await.unwrap_err()); + } + + Ok(()) + } +} diff --git a/src/models.rs b/src/models.rs index 7c591a59..a75f4a2b 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1176,3 +1176,11 @@ pub struct SocialAccount { pub provider: String, pub url: String, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SshSigningKey { + pub key: String, + pub id: u64, + pub title: String, + pub created_at: DateTime<Utc>, +} diff --git a/tests/resources/user_ssh_signing_key_created.json b/tests/resources/user_ssh_signing_key_created.json new file mode 100644 index 00000000..0887c0cb --- /dev/null +++ b/tests/resources/user_ssh_signing_key_created.json @@ -0,0 +1,7 @@ +{ + "key": "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234", + "id": 2, + "url": "https://api.github.com/user/keys/2", + "title": "ssh-rsa AAAAB3NzaC1yc2EAAA", + "created_at": "2020-06-11T21:31:57Z" +} diff --git a/tests/resources/user_ssh_signing_keys.json b/tests/resources/user_ssh_signing_keys.json new file mode 100644 index 00000000..3dbf1f69 --- /dev/null +++ b/tests/resources/user_ssh_signing_keys.json @@ -0,0 +1,16 @@ +[ + { + "key": "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234", + "id": 2, + "url": "https://api.github.com/user/keys/2", + "title": "ssh-rsa AAAAB3NzaC1yc2EAAA", + "created_at": "2020-06-11T21:31:57Z" + }, + { + "key": "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJy931234", + "id": 3, + "url": "https://api.github.com/user/keys/3", + "title": "ssh-rsa AAAAB3NzaC1yc2EAAB", + "created_at": "2020-07-11T21:31:57Z" + } +] diff --git a/tests/user_ssh_signing_keys_tests.rs b/tests/user_ssh_signing_keys_tests.rs new file mode 100644 index 00000000..504e0de1 --- /dev/null +++ b/tests/user_ssh_signing_keys_tests.rs @@ -0,0 +1,135 @@ +use http::StatusCode; +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; + +use mock_error::setup_error_handler; +use octocrab::models::SshSigningKey; +use octocrab::Octocrab; + +/// Tests API calls related to check runs of a specific commit. +mod mock_error; + +const SSH_SIGNING_KEY_ID: u64 = 42; + +async fn setup_ssh_signing_keys_mock( + http_method: &str, + mocked_path: &str, + template: ResponseTemplate, +) -> MockServer { + let mock_server = MockServer::start().await; + + Mock::given(method(http_method)) + .and(path(mocked_path)) + .respond_with(template.clone()) + .mount(&mock_server) + .await; + setup_error_handler( + &mock_server, + &format!("http method {http_method} on {mocked_path} was not received"), + ) + .await; + mock_server +} + +fn setup_octocrab(uri: &str) -> Octocrab { + Octocrab::builder().base_uri(uri).unwrap().build().unwrap() +} + +#[tokio::test] +async fn should_respond_to_get_ssh_signing_key() { + let mocked_response: SshSigningKey = + serde_json::from_str(include_str!("resources/user_ssh_signing_key_created.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&mocked_response); + let mock_server = setup_ssh_signing_keys_mock( + "GET", + format!("/user/ssh_signing_keys/{SSH_SIGNING_KEY_ID}").as_str(), + template, + ) + .await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_other_user") + .ssh_signing_keys() + .get(SSH_SIGNING_KEY_ID) + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + let response = result.unwrap(); + let id = response.id; + assert_eq!(id, 2); +} + +#[tokio::test] +async fn should_respond_to_ssh_signing_keys_list() { + let mocked_response: Vec<SshSigningKey> = + serde_json::from_str(include_str!("resources/user_ssh_signing_keys.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&mocked_response); + let mock_server = setup_ssh_signing_keys_mock("GET", "/user/ssh_signing_keys", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_other_user") + .ssh_signing_keys() + .per_page(42) + .page(3u32) + .list() + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + let response = result.unwrap(); + let id = response.items.first().unwrap().id; + assert_eq!(id, 2); +} + +#[tokio::test] +async fn should_respond_to_ssh_signing_keys_add() { + let mocked_response: SshSigningKey = + serde_json::from_str(include_str!("resources/user_ssh_signing_key_created.json")).unwrap(); + let template = ResponseTemplate::new(StatusCode::CREATED).set_body_json(&mocked_response); + let mock_server = setup_ssh_signing_keys_mock("POST", "/user/ssh_signing_keys", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_user") + .ssh_signing_keys() + .add( + "Assh-rsa AAAAB3NzaC1yc2EAA".to_string(), + "A2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv123".to_string(), + ) + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + let result = result.unwrap(); + assert_eq!(result.id, 2); +} + +#[tokio::test] +async fn should_respond_to_ssh_signing_key_delete() { + let template = ResponseTemplate::new(StatusCode::NO_CONTENT); + let mock_server = setup_ssh_signing_keys_mock( + "DELETE", + format!("/user/ssh_signing_keys/{SSH_SIGNING_KEY_ID}").as_str(), + template, + ) + .await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_user") + .ssh_signing_keys() + .delete(SSH_SIGNING_KEY_ID) + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); +}