diff --git a/src/api/users.rs b/src/api/users.rs index dffc125e..e56a4d2d 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -9,6 +9,7 @@ pub use self::follow::{ListUserFollowerBuilder, ListUserFollowingBuilder}; use self::user_repos::ListUserReposBuilder; use crate::api::users::user_blocks::BlockedUsersBuilder; 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::models::UserId; use crate::params::users::emails::EmailVisibilityState; @@ -17,6 +18,7 @@ use crate::{error, GitHubError, Octocrab}; mod follow; mod user_blocks; mod user_emails; +mod user_git_ssh_keys; mod user_gpg_keys; mod user_repos; @@ -192,4 +194,12 @@ impl<'octo> UserHandler<'octo> { pub fn gpg_keys(&self) -> UserGpgKeysOpsBuilder<'_, '_> { UserGpgKeysOpsBuilder::new(self) } + + ///Git SSH keys operations builder + ///* List public SSH keys for the authenticated user + ///* Create a public SSH key for the authenticated user + ///* Delete a public SSH key for the authenticated user + pub fn git_ssh_keys(&self) -> UserGitSshKeysOpsBuilder<'_, '_> { + UserGitSshKeysOpsBuilder::new(self) + } } diff --git a/src/api/users/user_git_ssh_keys.rs b/src/api/users/user_git_ssh_keys.rs new file mode 100644 index 00000000..f0a27512 --- /dev/null +++ b/src/api/users/user_git_ssh_keys.rs @@ -0,0 +1,152 @@ +use crate::api::users::UserHandler; +use crate::models::GitSshKey; +use crate::{FromResponse, Page}; + +#[derive(serde::Serialize)] +pub struct UserGitSshKeysOpsBuilder<'octo, 'b> { + #[serde(skip)] + handler: &'b UserHandler<'octo>, + #[serde(skip_serializing_if = "Option::is_none")] + per_page: Option, + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, +} + +impl<'octo, 'b> UserGitSshKeysOpsBuilder<'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) -> Self { + self.per_page = Some(per_page.into()); + self + } + + /// Page number of the results to fetch. + pub fn page(mut self, page: impl Into) -> Self { + self.page = Some(page.into()); + self + } + + ///## List public SSH keys for the authenticated user + ///OAuth app tokens and personal access tokens (classic) need the read:public_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: + ///* "Git SSH keys" user permissions (read) + /// + ///```no_run + /// use octocrab::models::GitSshKey; + /// use octocrab::{Page, Result}; + /// async fn run() -> Result> { + /// octocrab::instance() + /// .users("current_user") + /// .git_ssh_keys() + /// .per_page(42).page(3u32) + /// .list() + /// .await + /// } + pub async fn list(&self) -> crate::Result> { + let route = "/user/keys".to_string(); + self.handler.crab.get(route, Some(&self)).await + } + + ///## Create a public SSH key for the authenticated user + /// OAuth app tokens and personal access tokens (classic) need the `write:gpg_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: + ///* "Git SSH keys" user permissions (write) + /// + ///```no_run + /// use octocrab::models::GitSshKey; + /// use octocrab::Result; + /// async fn run() -> Result { + /// octocrab::instance() + /// .users("current_user") + /// .git_ssh_keys() + /// .add("ssh-rsa AAAAB3NzaC1yc2EAAA".to_string(), "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234".to_string()) + /// .await + /// } + pub async fn add(&self, title: String, key: String) -> crate::Result { + let route = "/user/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()); + } + + ::from_response(crate::map_github_error(response).await?).await + } + + ///## Delete a public SSH key for the authenticated user + /// OAuth app tokens and personal access tokens (classic) need the `admin:public_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: + ///* "Git SSH keys" user permissions (write) + /// + ///```no_run + /// use octocrab::Result; + /// async fn run() -> Result<()> { + /// octocrab::instance() + /// .users("current_user") + /// .git_ssh_keys() + /// .delete(42) + /// .await + /// } + pub async fn delete(&self, git_ssh_key_id: u64) -> crate::Result<()> { + let route = format!("/user/keys/{git_ssh_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(()) + } + + ///## Get a public SSH key for the authenticated user + /// + ///OAuth app tokens and personal access tokens (classic) need the `read:public_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: + ///* "Git SSH keys" user permissions (read) + /// + ///```no_run + /// use octocrab::models::GitSshKey; + /// use octocrab::Result; + /// async fn run() -> Result { + /// octocrab::instance() + /// .users("current_user") + /// .git_ssh_keys() + /// .get(42) + /// .await + /// } + pub async fn get(&self, git_ssh_key_id: u64) -> crate::Result { + let route = format!("/user/keys/{git_ssh_key_id}"); + self.handler.crab.get(route, None::<&()>).await + } +} diff --git a/src/api/users/user_gpg_keys.rs b/src/api/users/user_gpg_keys.rs index b210404c..c0d15e9e 100644 --- a/src/api/users/user_gpg_keys.rs +++ b/src/api/users/user_gpg_keys.rs @@ -1,7 +1,6 @@ use crate::api::users::UserHandler; -use crate::models::{GpgKey, UserEmailInfo}; +use crate::models::GpgKey; use crate::{FromResponse, Page}; -use std::fmt::format; #[derive(serde::Serialize)] pub struct UserGpgKeysOpsBuilder<'octo, 'b> { diff --git a/src/models.rs b/src/models.rs index d9f52ded..edffb0cc 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1159,3 +1159,14 @@ pub struct GpgKey { #[serde(skip_serializing_if = "Option::is_none")] pub raw_key: Option, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GitSshKey { + pub key: String, + pub id: u64, + pub url: String, + pub title: String, + pub created_at: DateTime, + pub verified: bool, + pub read_only: bool, +} diff --git a/tests/resources/user_git_ssh_key_created.json b/tests/resources/user_git_ssh_key_created.json new file mode 100644 index 00000000..f2227c3a --- /dev/null +++ b/tests/resources/user_git_ssh_key_created.json @@ -0,0 +1,9 @@ +{ + "key": "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234", + "id": 2, + "url": "https://api.github.com/user/keys/2", + "title": "ssh-rsa AAAAB3NzaC1yc2EAAA", + "created_at": "2020-06-11T21:31:57Z", + "verified": false, + "read_only": false +} diff --git a/tests/resources/user_git_ssh_keys.json b/tests/resources/user_git_ssh_keys.json new file mode 100644 index 00000000..45487515 --- /dev/null +++ b/tests/resources/user_git_ssh_keys.json @@ -0,0 +1,20 @@ +[ + { + "key": "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234", + "id": 2, + "url": "https://api.github.com/user/keys/2", + "title": "ssh-rsa AAAAB3NzaC1yc2EAAA", + "created_at": "2020-06-11T21:31:57Z", + "verified": false, + "read_only": false + }, + { + "key": "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJy931234", + "id": 3, + "url": "https://api.github.com/user/keys/3", + "title": "ssh-rsa AAAAB3NzaC1yc2EAAB", + "created_at": "2020-07-11T21:31:57Z", + "verified": false, + "read_only": false + } +] diff --git a/tests/user_git_ssh_keys_tests.rs b/tests/user_git_ssh_keys_tests.rs new file mode 100644 index 00000000..ef364cf4 --- /dev/null +++ b/tests/user_git_ssh_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::GitSshKey; +use octocrab::Octocrab; + +/// Tests API calls related to check runs of a specific commit. +mod mock_error; + +const GIT_SSH_KEY_ID: u64 = 42; + +async fn setup_git_ssh_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_git_ssh_key() { + let mocked_response: GitSshKey = + serde_json::from_str(include_str!("resources/user_git_ssh_key_created.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&mocked_response); + let mock_server = setup_git_ssh_keys_mock( + "GET", + format!("/user/keys/{GIT_SSH_KEY_ID}").as_str(), + template, + ) + .await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_other_user") + .git_ssh_keys() + .get(GIT_SSH_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_git_ssh_keys_list() { + let mocked_response: Vec = + serde_json::from_str(include_str!("resources/user_git_ssh_keys.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&mocked_response); + let mock_server = setup_git_ssh_keys_mock("GET", "/user/keys", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_other_user") + .git_ssh_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_git_ssh_keys_add() { + let mocked_response: GitSshKey = + serde_json::from_str(include_str!("resources/user_git_ssh_key_created.json")).unwrap(); + let template = ResponseTemplate::new(StatusCode::CREATED).set_body_json(&mocked_response); + let mock_server = setup_git_ssh_keys_mock("POST", "/user/keys", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_user") + .git_ssh_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_git_ssh_key_delete() { + let template = ResponseTemplate::new(StatusCode::NO_CONTENT); + let mock_server = setup_git_ssh_keys_mock( + "DELETE", + format!("/user/keys/{GIT_SSH_KEY_ID}").as_str(), + template, + ) + .await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_user") + .git_ssh_keys() + .delete(GIT_SSH_KEY_ID) + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); +} diff --git a/tests/user_gpg_keys_tests.rs b/tests/user_gpg_keys_tests.rs index 68d8b7bb..2679ed6a 100644 --- a/tests/user_gpg_keys_tests.rs +++ b/tests/user_gpg_keys_tests.rs @@ -5,8 +5,7 @@ use wiremock::{ }; use mock_error::setup_error_handler; -use octocrab::models::{GpgKey, UserEmailInfo}; -use octocrab::params::users::emails::EmailVisibilityState; +use octocrab::models::GpgKey; use octocrab::Octocrab; /// Tests API calls related to check runs of a specific commit.