From 1d33909bbbe1c6a908d4f8a69608c0837c390134 Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Thu, 10 Oct 2024 15:56:31 +0300 Subject: [PATCH] added user's gpg_keys operations --- src/api/users.rs | 11 ++ src/api/users/user_gpg_keys.rs | 147 ++++++++++++++++++++++ src/models.rs | 51 ++++++++ tests/resources/user_gpg_key_created.json | 37 ++++++ tests/resources/user_gpg_keys.json | 39 ++++++ tests/user_gpg_keys_tests.rs | 136 ++++++++++++++++++++ 6 files changed, 421 insertions(+) create mode 100644 src/api/users/user_gpg_keys.rs create mode 100644 tests/resources/user_gpg_key_created.json create mode 100644 tests/resources/user_gpg_keys.json create mode 100644 tests/user_gpg_keys_tests.rs diff --git a/src/api/users.rs b/src/api/users.rs index cda84b7d..dffc125e 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_gpg_keys::UserGpgKeysOpsBuilder; use crate::models::UserId; use crate::params::users::emails::EmailVisibilityState; use crate::{error, GitHubError, Octocrab}; @@ -16,6 +17,7 @@ use crate::{error, GitHubError, Octocrab}; mod follow; mod user_blocks; mod user_emails; +mod user_gpg_keys; mod user_repos; pub(crate) enum UserRef { @@ -181,4 +183,13 @@ impl<'octo> UserHandler<'octo> { pub fn emails(&self) -> UserEmailsOpsBuilder<'_, '_> { UserEmailsOpsBuilder::new(self) } + + ///GPG Keys operations builder + ///* List GPG keys for the authenticated user + ///* Get a GPG key for the authenticated user + ///* Add an GPG key for the authenticated user + ///* Delete a GPG key for the authenticated user + pub fn gpg_keys(&self) -> UserGpgKeysOpsBuilder<'_, '_> { + UserGpgKeysOpsBuilder::new(self) + } } diff --git a/src/api/users/user_gpg_keys.rs b/src/api/users/user_gpg_keys.rs new file mode 100644 index 00000000..b210404c --- /dev/null +++ b/src/api/users/user_gpg_keys.rs @@ -0,0 +1,147 @@ +use crate::api::users::UserHandler; +use crate::models::{GpgKey, UserEmailInfo}; +use crate::{FromResponse, Page}; +use std::fmt::format; + +#[derive(serde::Serialize)] +pub struct UserGpgKeysOpsBuilder<'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> UserGpgKeysOpsBuilder<'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 GPG keys for the authenticated user + ///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: + ///* "GPG keys" user permissions (read) + /// + ///```no_run + /// use octocrab::models::GpgKey; + /// use octocrab::{Page, Result}; + /// async fn run() -> Result> { + /// octocrab::instance() + /// .users("current_user") + /// .gpg_keys() + /// .per_page(42).page(3u32) + /// .list() + /// .await + /// } + pub async fn list(&self) -> crate::Result> { + let route = "/user/gpg_keys".to_string(); + self.handler.crab.get(route, Some(&self)).await + } + + ///## View extended details for a single GPG key for the authenticated user + /// + ///OAuth app tokens and personal access tokens (classic) need the read:gpg_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: + ///* "GPG keys" user permissions (read) + /// + ///```no_run + /// use octocrab::models::GpgKey; + /// use octocrab::Result; + /// async fn run() -> Result { + /// octocrab::instance() + /// .users("current_user") + /// .gpg_keys() + /// .get(42) + /// .await + /// } + pub async fn get(&self, gpg_key_id: u64) -> crate::Result { + let route = format!("/user/gpg_keys/{gpg_key_id}"); + self.handler.crab.get(route, None::<&()>).await + } + + ///## Create a GPG key 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) + /// + ///The fine-grained token must have the following permission set: + ///* "GPG keys" user permissions (write) + /// + ///```no_run + /// use octocrab::models::GpgKey; + /// use octocrab::Result; + /// async fn run() -> Result { + /// octocrab::instance() + /// .users("current_user") + /// .gpg_keys() + /// .add("descriptive name".to_string(), "".to_string()) + /// .await + /// } + pub async fn add(&self, name: String, armored_public_key: String) -> crate::Result { + let route = "/user/gpg_keys".to_string(); + + let params = serde_json::json!({ + "name": name, + "armored_public_key": armored_public_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 GPG key 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) + /// + ///The fine-grained token must have the following permission set: + ///* "GPG keys" user permissions (write) + /// + ///```no_run + /// use octocrab::Result; + /// async fn run() -> Result<()> { + /// octocrab::instance() + /// .users("current_user") + /// .gpg_keys() + /// .delete(42) + /// .await + /// } + pub async fn delete(&self, gpg_key_id: u64) -> crate::Result<()> { + let route = format!("/user/gpg_keys/{gpg_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 687fbf17..d9f52ded 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1108,3 +1108,54 @@ pub struct UserEmailInfo { pub verified: bool, pub visibility: EmailVisibilityState, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VerifiedEmailInfo { + pub email: String, + pub verified: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SubKeyInfo { + pub id: u64, + pub primary_key_id: u64, + pub key_id: String, + pub public_key: String, + pub emails: Vec, + pub subkeys: Option>, + pub can_sign: bool, + pub can_encrypt_comms: bool, + pub can_encrypt_storage: bool, + pub can_certify: bool, + pub created_at: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub raw_key: Option, + pub revoked: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GpgKey { + pub id: u64, + pub name: String, + pub primary_key_id: u64, + pub key_id: String, + pub public_key: String, + pub emails: Vec, + pub subkeys: Vec, + pub can_sign: bool, + pub can_encrypt_comms: bool, + pub can_encrypt_storage: bool, + pub can_certify: bool, + pub created_at: DateTime, + #[serde( + skip_serializing_if = "Option::is_none", + default, + deserialize_with = "date_serde::deserialize_opt" + )] + pub expires_at: Option>, + pub revoked: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub raw_key: Option, +} diff --git a/tests/resources/user_gpg_key_created.json b/tests/resources/user_gpg_key_created.json new file mode 100644 index 00000000..d3343381 --- /dev/null +++ b/tests/resources/user_gpg_key_created.json @@ -0,0 +1,37 @@ +{ + "id": 3, + "name": "Octocat's GPG Key", + "primary_key_id": 2, + "key_id": "3262EFF25BA0D270", + "public_key": "xsBNBFayYZ...", + "emails": [ + { + "email": "octocat@users.noreply.github.com", + "verified": true + } + ], + "subkeys": [ + { + "id": 4, + "primary_key_id": 3, + "key_id": "4A595D4C72EE49C7", + "public_key": "zsBNBFayYZ...", + "emails": [], + "can_sign": false, + "can_encrypt_comms": true, + "can_encrypt_storage": true, + "can_certify": false, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": false + } + ], + "can_sign": true, + "can_encrypt_comms": false, + "can_encrypt_storage": false, + "can_certify": true, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": false, + "raw_key": "\"-----BEGIN PGP PUBLIC KEY BLOCK-----\\nVersion: GnuPG v2\\n\\n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\\nIts/HFYRLiFgDLmTlxo=\\n=+OzK\\n-----END PGP PUBLIC KEY BLOCK-----\"" +} diff --git a/tests/resources/user_gpg_keys.json b/tests/resources/user_gpg_keys.json new file mode 100644 index 00000000..21a4003f --- /dev/null +++ b/tests/resources/user_gpg_keys.json @@ -0,0 +1,39 @@ +[ + { + "id": 3, + "name": "Octocat's GPG Key", + "primary_key_id": 2, + "key_id": "3262EFF25BA0D270", + "public_key": "xsBNBFayYZ...", + "emails": [ + { + "email": "octocat@users.noreply.github.com", + "verified": true + } + ], + "subkeys": [ + { + "id": 4, + "primary_key_id": 3, + "key_id": "4A595D4C72EE49C7", + "public_key": "zsBNBFayYZ...", + "emails": [], + "can_sign": false, + "can_encrypt_comms": true, + "can_encrypt_storage": true, + "can_certify": false, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": false + } + ], + "can_sign": true, + "can_encrypt_comms": false, + "can_encrypt_storage": false, + "can_certify": true, + "created_at": "2016-03-24T11:31:04-06:00", + "expires_at": "2016-03-24T11:31:04-07:00", + "revoked": false, + "raw_key": "string" + } +] diff --git a/tests/user_gpg_keys_tests.rs b/tests/user_gpg_keys_tests.rs new file mode 100644 index 00000000..68d8b7bb --- /dev/null +++ b/tests/user_gpg_keys_tests.rs @@ -0,0 +1,136 @@ +use http::StatusCode; +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; + +use mock_error::setup_error_handler; +use octocrab::models::{GpgKey, UserEmailInfo}; +use octocrab::params::users::emails::EmailVisibilityState; +use octocrab::Octocrab; + +/// Tests API calls related to check runs of a specific commit. +mod mock_error; + +const GPG_KEY_ID: u64 = 42; + +async fn setup_gpg_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_gpg_key() { + let mocked_response: GpgKey = + serde_json::from_str(include_str!("resources/user_gpg_key_created.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&mocked_response); + let mock_server = setup_gpg_keys_mock( + "GET", + format!("/user/gpg_keys/{GPG_KEY_ID}").as_str(), + template, + ) + .await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_other_user") + .gpg_keys() + .get(GPG_KEY_ID) + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + let response = result.unwrap(); + let name = response.name; + assert_eq!(name, "Octocat's GPG Key"); +} + +#[tokio::test] +async fn should_respond_to_gpg_keys_list() { + let mocked_response: Vec = + serde_json::from_str(include_str!("resources/user_gpg_keys.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&mocked_response); + let mock_server = setup_gpg_keys_mock("GET", "/user/gpg_keys", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_other_user") + .gpg_keys() + .per_page(42) + .page(3u32) + .list() + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + let response = result.unwrap(); + let name = &response.items.first().unwrap().name; + assert_eq!(name, "Octocat's GPG Key"); +} + +#[tokio::test] +async fn should_respond_to_gpg_keys_add() { + let mocked_response: GpgKey = + serde_json::from_str(include_str!("resources/user_gpg_key_created.json")).unwrap(); + let template = ResponseTemplate::new(StatusCode::CREATED).set_body_json(&mocked_response); + let mock_server = setup_gpg_keys_mock("POST", "/user/gpg_keys", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_user") + .gpg_keys() + .add( + "A descriptive name for the new key".to_string(), + "A GPG key in ASCII-armored format".to_string(), + ) + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + let result = result.unwrap(); + assert_eq!(result.name, "Octocat's GPG Key"); +} + +#[tokio::test] +async fn should_respond_to_gpg_key_delete() { + let template = ResponseTemplate::new(StatusCode::NO_CONTENT); + let mock_server = setup_gpg_keys_mock( + "DELETE", + format!("/user/gpg_keys/{GPG_KEY_ID}").as_str(), + template, + ) + .await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_user") + .gpg_keys() + .delete(GPG_KEY_ID) + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); +}