From 6faf19816d8f1fe632b0c2cae6916abb098111e8 Mon Sep 17 00:00:00 2001 From: Dmytro Horskyi Date: Wed, 9 Oct 2024 19:03:09 +0300 Subject: [PATCH] #552 (emails, public_emails, visibility) (#712) --- src/api/users.rs | 46 +++++++++- src/api/users/user_emails.rs | 151 +++++++++++++++++++++++++++++++ src/models.rs | 9 ++ src/params.rs | 13 +++ tests/resources/user_emails.json | 8 ++ tests/user_emails_tests.rs | 142 +++++++++++++++++++++++++++++ 6 files changed, 366 insertions(+), 3 deletions(-) create mode 100644 src/api/users/user_emails.rs create mode 100644 tests/resources/user_emails.json create mode 100644 tests/user_emails_tests.rs diff --git a/src/api/users.rs b/src/api/users.rs index 0a005c09..cda84b7d 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -5,15 +5,17 @@ use std::backtrace::Backtrace; use http::StatusCode; use snafu::GenerateImplicitData; +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::models::UserId; +use crate::params::users::emails::EmailVisibilityState; use crate::{error, GitHubError, Octocrab}; -pub use self::follow::{ListUserFollowerBuilder, ListUserFollowingBuilder}; -use self::user_repos::ListUserReposBuilder; - mod follow; mod user_blocks; +mod user_emails; mod user_repos; pub(crate) enum UserRef { @@ -141,4 +143,42 @@ impl<'octo> UserHandler<'octo> { self.crab.delete(route, None::<&()>).await } + + ///## Set primary email visibility 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: + /// + ///"Email addresses" user permissions (write) + /// + ///```no_run + /// use octocrab::params::users::emails::EmailVisibilityState::*; + /// use octocrab::models::UserEmailInfo; + /// + /// async fn run() -> octocrab::Result> { + /// octocrab::instance() + /// .users("current_user") + /// .set_primary_email_visibility(Public) // or Private + /// .await + /// } + pub async fn set_primary_email_visibility( + &self, + visibility: EmailVisibilityState, + ) -> crate::Result> { + let route = String::from("/user/email/visibility"); + let params = serde_json::json!({ + "visibility": serde_json::to_string(&visibility).unwrap(), + }); + self.crab.patch(route, Some(¶ms)).await + } + + ///Email addresses operations builder + ///* List email addresses for the authenticated user + ///* Add an email address for the authenticated user + ///* Delete an email address for the authenticated user + pub fn emails(&self) -> UserEmailsOpsBuilder<'_, '_> { + UserEmailsOpsBuilder::new(self) + } } diff --git a/src/api/users/user_emails.rs b/src/api/users/user_emails.rs new file mode 100644 index 00000000..b964790f --- /dev/null +++ b/src/api/users/user_emails.rs @@ -0,0 +1,151 @@ +use crate::api::users::UserHandler; +use crate::models::UserEmailInfo; +use crate::{FromResponse, Page}; + +#[derive(serde::Serialize)] +pub struct UserEmailsOpsBuilder<'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> UserEmailsOpsBuilder<'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 email addresses 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: + ///* "Email addresses" user permissions (read) + /// + ///```no_run + /// use octocrab::models::UserEmailInfo; + /// use octocrab::{Page, Result}; + /// async fn run() -> Result> { + /// octocrab::instance() + /// .users("current_user") + /// .emails() + /// .per_page(42).page(3u32) + /// .list() + /// .await + /// } + pub async fn list(&self) -> crate::Result> { + let route = "/user/emails".to_string(); + self.handler.crab.get(route, Some(&self)).await + } + + ///## List public email addresses for the authenticated user + /// Lists your publicly visible email address, which you can set with the `Set primary email visibility`. + ///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: + ///* "Email addresses" user permissions (read) + /// This method can be used without authentication or the aforementioned permissions if only public resources are requested. + /// + ///```no_run + /// use octocrab::models::UserEmailInfo; + /// use octocrab::{Page, Result}; + /// async fn run() -> Result> { + /// octocrab::instance() + /// .users("current_user") + /// .emails() + /// .per_page(42).page(3u32) + /// .list_public() + /// .await + /// } + pub async fn list_public(&self) -> crate::Result> { + let route = "/user/public_emails".to_string(); + self.handler.crab.get(route, Some(&self)).await + } + + ///## Add an email address(es) 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: + ///* "Email addresses" user permissions (write) + /// + ///```no_run + /// use octocrab::models::UserEmailInfo; + /// use octocrab::Result; + /// async fn run() -> Result> { + /// octocrab::instance() + /// .users("current_user") + /// .emails() + /// .add(vec!["newemail1@mail.com".to_string(), "newemail2@mail.com".to_string()]) + /// .await + /// } + pub async fn add( + &self, + emails: Vec, + ) -> crate::Result> { + let route = "/user/emails".to_string(); + + let params = serde_json::json!({ + "emails": serde_json::Value::from(emails), + }); + 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 an email address(es) 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: + ///* "Email addresses" user permissions (write) + /// + ///```no_run + /// use octocrab::Result; + /// async fn run() -> Result<()> { + /// octocrab::instance() + /// .users("current_user") + /// .emails() + /// .delete(vec!["newemail1@mail.com".to_string(), "newemail2@mail.com".to_string()]) + /// .await + /// } + pub async fn delete(&self, emails: Vec) -> crate::Result<()> { + let route = "/user/emails".to_string(); + + let params = serde_json::json!({ + "emails": serde_json::Value::from(emails), + }); + let response = self.handler.crab._delete(route, Some(¶ms)).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 7d55c8be..687fbf17 100644 --- a/src/models.rs +++ b/src/models.rs @@ -8,6 +8,7 @@ use chrono::{DateTime, Utc}; use serde::{de, Deserialize, Deserializer, Serialize}; use url::Url; +use crate::params::users::emails::EmailVisibilityState; pub use apps::App; pub mod actions; @@ -1099,3 +1100,11 @@ pub struct Rate { pub remaining: usize, pub reset: u64, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserEmailInfo { + pub email: String, + pub primary: bool, + pub verified: bool, + pub visibility: EmailVisibilityState, +} diff --git a/src/params.rs b/src/params.rs index 21acdf41..6bae8590 100644 --- a/src/params.rs +++ b/src/params.rs @@ -603,4 +603,17 @@ pub mod users { Member, } } + + pub mod emails { + use serde::{Deserialize, Serialize}; + + ///Denotes whether an email is publicly visible. + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy)] + #[serde(rename_all = "snake_case")] + #[non_exhaustive] + pub enum EmailVisibilityState { + Public, + Private, + } + } } diff --git a/tests/resources/user_emails.json b/tests/resources/user_emails.json new file mode 100644 index 00000000..75478793 --- /dev/null +++ b/tests/resources/user_emails.json @@ -0,0 +1,8 @@ +[ + { + "email": "octocat@github.com", + "primary": true, + "verified": true, + "visibility": "private" + } +] diff --git a/tests/user_emails_tests.rs b/tests/user_emails_tests.rs new file mode 100644 index 00000000..baa78bca --- /dev/null +++ b/tests/user_emails_tests.rs @@ -0,0 +1,142 @@ +use http::StatusCode; +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; + +use mock_error::setup_error_handler; +use octocrab::models::UserEmailInfo; +use octocrab::params::users::emails::EmailVisibilityState; +use octocrab::Octocrab; + +/// Tests API calls related to check runs of a specific commit. +mod mock_error; + +async fn setup_emails_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_primary_email_visibility() { + let mocked_response: Vec = + serde_json::from_str(include_str!("resources/user_emails.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&mocked_response); + let mock_server = setup_emails_mock("PATCH", "/user/email/visibility", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_other_user") + .set_primary_email_visibility(EmailVisibilityState::Private) + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + let response = result.unwrap(); + let visibility = response.first().unwrap().visibility; + assert_eq!(visibility, EmailVisibilityState::Private); +} + +#[tokio::test] +async fn should_respond_to_email_list() { + let mocked_response: Vec = + serde_json::from_str(include_str!("resources/user_emails.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&mocked_response); + let mock_server = setup_emails_mock("GET", "/user/emails", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_other_user") + .emails() + .per_page(42) + .page(3u32) + .list() + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + let response = result.unwrap(); + let visibility = response.items.first().unwrap().visibility; + assert_eq!(visibility, EmailVisibilityState::Private); +} + +#[tokio::test] +async fn should_respond_to_public_email_list() { + let mocked_response: Vec = + serde_json::from_str(include_str!("resources/user_emails.json")).unwrap(); + let template = ResponseTemplate::new(200).set_body_json(&mocked_response); + let mock_server = setup_emails_mock("GET", "/user/public_emails", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_other_user") + .emails() + .per_page(42) + .page(3u32) + .list_public() + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + let response = result.unwrap(); + let visibility = response.items.first().unwrap().visibility; + assert_eq!(visibility, EmailVisibilityState::Private); +} + +#[tokio::test] +async fn should_respond_to_emails_add() { + let mocked_response: Vec = + serde_json::from_str(include_str!("resources/user_emails.json")).unwrap(); + let template = ResponseTemplate::new(StatusCode::CREATED).set_body_json(&mocked_response); + let mock_server = setup_emails_mock("POST", "/user/emails", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_other_user") + .emails() + .add(vec!["newemail1@mail.com".to_string()]) + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); +} + +#[tokio::test] +async fn should_respond_to_emails_delete() { + let template = ResponseTemplate::new(StatusCode::NO_CONTENT); + let mock_server = setup_emails_mock("DELETE", "/user/emails", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_other_user") + .emails() + .delete(vec!["newemail1@mail.com".to_string()]) + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); +}