Skip to content

Commit

Permalink
added user's social accounts operations (/user/social_accounts) (#723)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmgorsky authored Oct 15, 2024
1 parent d676963 commit 766962c
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 0 deletions.
10 changes: 10 additions & 0 deletions src/api/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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),
Expand Down Expand Up @@ -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)
}
}
128 changes: 128 additions & 0 deletions src/api/users/user_social_accounts.rs
Original file line number Diff line number Diff line change
@@ -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<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
page: Option<u32>,
}

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<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 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<Page<SocialAccount>> {
/// octocrab::instance()
/// .users("current_user")
/// .social_accounts()
/// .per_page(42).page(3u32)
/// .list()
/// .await
/// }
pub async fn list(&self) -> crate::Result<Page<crate::models::SocialAccount>> {
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<Vec<SocialAccount>> {
/// 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<String>) -> crate::Result<Vec<SocialAccount>> {
let route = "/user/social_accounts".to_string();

let params = serde_json::json!({
"account_urls": account_urls,
});
let response = self.handler.crab._post(route, Some(&params)).await?;
if response.status() != http::StatusCode::CREATED {
return Err(crate::map_github_error(response).await.unwrap_err());
}

<Vec<SocialAccount>>::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<String>) -> 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(&params)).await?;
if response.status() != http::StatusCode::NO_CONTENT {
return Err(crate::map_github_error(response).await.unwrap_err());
}

Ok(())
}
}
6 changes: 6 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
6 changes: 6 additions & 0 deletions tests/resources/user_social_accounts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"provider": "twitter",
"url": "https://twitter.com/github"
}
]
104 changes: 104 additions & 0 deletions tests/user_social_accounts_tests.rs
Original file line number Diff line number Diff line change
@@ -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<SocialAccount> =
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<SocialAccount> =
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
);
}

0 comments on commit 766962c

Please sign in to comment.