diff --git a/src/api/users.rs b/src/api/users.rs index e56a4d2d..d6e0769c 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -11,6 +11,7 @@ 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::api::users::user_social_accounts::UserSocialAccountsOpsBuilder; use crate::models::UserId; use crate::params::users::emails::EmailVisibilityState; use crate::{error, GitHubError, Octocrab}; @@ -21,6 +22,7 @@ mod user_emails; mod user_git_ssh_keys; mod user_gpg_keys; mod user_repos; +mod user_social_accounts; pub(crate) enum UserRef { ByString(String), @@ -202,4 +204,12 @@ impl<'octo> UserHandler<'octo> { pub fn git_ssh_keys(&self) -> UserGitSshKeysOpsBuilder<'_, '_> { UserGitSshKeysOpsBuilder::new(self) } + + ///Social accounts operations builder + ///* List social accounts for the authenticated user + ///* Add social accounts for the authenticated user + ///* Delete social accounts for the authenticated user + pub fn social_accounts(&self) -> UserSocialAccountsOpsBuilder<'_, '_> { + UserSocialAccountsOpsBuilder::new(self) + } } diff --git a/src/api/users/user_social_accounts.rs b/src/api/users/user_social_accounts.rs new file mode 100644 index 00000000..d8cf92c6 --- /dev/null +++ b/src/api/users/user_social_accounts.rs @@ -0,0 +1,128 @@ +use crate::api::users::UserHandler; +use crate::models::SocialAccount; +use crate::{FromResponse, Page}; + +#[derive(serde::Serialize)] +pub struct UserSocialAccountsOpsBuilder<'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> UserSocialAccountsOpsBuilder<'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 social accounts 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 does not require any permissions. + /// + ///```no_run + /// use octocrab::models::SocialAccount; + /// use octocrab::{Page, Result}; + /// async fn run() -> Result> { + /// octocrab::instance() + /// .users("current_user") + /// .social_accounts() + /// .per_page(42).page(3u32) + /// .list() + /// .await + /// } + pub async fn list(&self) -> crate::Result> { + let route = "/user/social_accounts".to_string(); + self.handler.crab.get(route, Some(&self)).await + } + + ///## Add social accounts for the authenticated user + ///OAuth app tokens and personal access tokens (classic) need the `user` 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: + ///* "Profile" user permissions (write) + /// + ///```no_run + /// use octocrab::models::SocialAccount; + /// use octocrab::Result; + /// async fn run() -> Result> { + /// octocrab::instance() + /// .users("current_user") + /// .social_accounts() + /// .add(vec!["https://facebook.com/GitHub".to_string(),"https://www.youtube.com/@GitHub".to_string()]) + /// .await + /// } + pub async fn add(&self, account_urls: Vec) -> crate::Result> { + let route = "/user/social_accounts".to_string(); + + let params = serde_json::json!({ + "account_urls": account_urls, + }); + 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 + } + + ///## Deletes one or more social accounts from the authenticated user's profile. + // + // OAuth app tokens and personal access tokens (classic) need the `user` 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: + ///* "Profile" user permissions (write) + /// + ///```no_run + /// use octocrab::Result; + /// async fn run() -> Result<()> { + /// octocrab::instance() + /// .users("current_user") + /// .social_accounts() + /// .delete(vec!["https://facebook.com/GitHub".to_string(),"https://www.youtube.com/@GitHub".to_string()]) + /// .await + /// } + pub async fn delete(&self, account_urls: Vec) -> crate::Result<()> { + let route = "/user/social_accounts".to_string(); + + let params = serde_json::json!({ + "account_urls": account_urls, + }); + + 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 edffb0cc..7c591a59 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1170,3 +1170,9 @@ pub struct GitSshKey { pub verified: bool, pub read_only: bool, } + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SocialAccount { + pub provider: String, + pub url: String, +} diff --git a/tests/resources/user_social_accounts.json b/tests/resources/user_social_accounts.json new file mode 100644 index 00000000..6550c3ea --- /dev/null +++ b/tests/resources/user_social_accounts.json @@ -0,0 +1,6 @@ +[ + { + "provider": "twitter", + "url": "https://twitter.com/github" + } +] diff --git a/tests/user_social_accounts_tests.rs b/tests/user_social_accounts_tests.rs new file mode 100644 index 00000000..95f5a26c --- /dev/null +++ b/tests/user_social_accounts_tests.rs @@ -0,0 +1,104 @@ +use http::StatusCode; +use wiremock::{ + matchers::{method, path}, + Mock, MockServer, ResponseTemplate, +}; + +use mock_error::setup_error_handler; +use octocrab::models::SocialAccount; +use octocrab::Octocrab; + +/// Tests API calls related to check runs of a specific commit. +mod mock_error; + +async fn setup_social_accounts_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_social_accounts_list() { + let mocked_response: Vec = + serde_json::from_str(include_str!("resources/user_social_accounts.json")).unwrap(); + let template = ResponseTemplate::new(StatusCode::OK).set_body_json(&mocked_response); + let mock_server = setup_social_accounts_mock("GET", "/user/social_accounts", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_other_user") + .social_accounts() + .per_page(42) + .page(3u32) + .list() + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + let response = result.unwrap(); + let provider = &response.items.first().unwrap().provider; + assert_eq!(provider, "twitter"); +} + +#[tokio::test] +async fn should_respond_to_social_accounts_add() { + let mocked_response: Vec = + serde_json::from_str(include_str!("resources/user_social_accounts.json")).unwrap(); + let template = ResponseTemplate::new(StatusCode::CREATED).set_body_json(&mocked_response); + let mock_server = setup_social_accounts_mock("POST", "/user/social_accounts", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_user") + .social_accounts() + .add(vec![ + "https://facebook.com/GitHub".to_string(), + "https://www.youtube.com/@GitHub".to_string(), + ]) + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); + let result = result.unwrap(); + assert_eq!(result.first().unwrap().provider, "twitter"); +} + +#[tokio::test] +async fn should_respond_to_social_account_delete() { + let template = ResponseTemplate::new(StatusCode::NO_CONTENT); + let mock_server = setup_social_accounts_mock("DELETE", "/user/social_accounts", template).await; + let client = setup_octocrab(&mock_server.uri()); + let result = client + .users("some_user") + .social_accounts() + .delete(vec![ + "https://facebook.com/GitHub".to_string(), + "https://www.youtube.com/@GitHub".to_string(), + ]) + .await; + assert!( + result.is_ok(), + "expected successful result, got error: {:#?}", + result + ); +}