Skip to content

Commit

Permalink
feat: added /user/blocks functionality (#657)
Browse files Browse the repository at this point in the history
* added /user/blocks functionality (is_blocked, block_user, unblock_user, list_blocked)

* Update issues.rs (#634)

* fix(builder): Change add_retry_config signature to match others in OctocrabBuilder (#643)

* Fix issue #635 (#637)

---------

Co-authored-by: Artur Yurii Korchynskyi <[email protected]>
Co-authored-by: Kleo Davidson <[email protected]>
Co-authored-by: Hans Avontuur <[email protected]>
  • Loading branch information
4 people authored Jun 17, 2024
1 parent 130a6e7 commit 736ccc1
Show file tree
Hide file tree
Showing 5 changed files with 320 additions and 6 deletions.
95 changes: 91 additions & 4 deletions src/api/users.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
//! The users API.
use std::backtrace::Backtrace;

use http::StatusCode;
use snafu::GenerateImplicitData;

use crate::api::users::user_blocks::BlockedUsersBuilder;
use crate::{error, GitHubError, Octocrab};

pub use self::follow::{ListUserFollowerBuilder, ListUserFollowingBuilder};
use self::user_repos::ListUserReposBuilder;

mod follow;
mod user_blocks;
mod user_repos;

use self::follow::{ListUserFollowerBuilder, ListUserFollowingBuilder};
pub use self::user_repos::ListUserReposBuilder;
use crate::Octocrab;

pub struct UserHandler<'octo> {
crab: &'octo Octocrab,
user: String,
Expand Down Expand Up @@ -38,4 +46,83 @@ impl<'octo> UserHandler<'octo> {
pub fn repos(&self) -> ListUserReposBuilder<'_, '_> {
ListUserReposBuilder::new(self)
}

/// API for listing blocked users
/// you must pass authentication information with your requests
pub fn blocks(&self) -> BlockedUsersBuilder {
BlockedUsersBuilder::new(self)
}

///## Check if a user is blocked by 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 token must have the following permission set: `blocking:read`
///
///```no_run
/// async fn run() -> octocrab::Result<bool> {
/// let is_blocked = octocrab::instance()
/// .users("current_user")
/// .is_blocked("some_user")
/// .await?;
/// Ok(is_blocked)
/// }
pub async fn is_blocked(&self, username: &str) -> crate::Result<bool> {
let route = format!("/user/blocks/{}", username);
let response = self.crab._get(route).await?;
Ok(response.status() == 204)
}

///## Blocks the given 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 token must have the following permission set: `blocking:read`
///
///```no_run
/// async fn run() -> octocrab::Result<()> {
/// octocrab::instance()
/// .users("current_user")
/// .block_user("some_user")
/// .await
/// }
pub async fn block_user(&self, username: &str) -> crate::Result<()> {
let route = format!("/user/blocks/{}", username);
/* '204 not found' is returned if user blocked */
let result: crate::Result<()> = self.crab.put(route, None::<&()>).await;
match result {
Ok(_) => Err(error::Error::GitHub {
source: GitHubError {
status_code: StatusCode::OK,
documentation_url: None,
errors: None,
message: "".to_string(),
},
backtrace: Backtrace::generate(),
}),
Err(_v) => Ok(()),
}
}

///## Unblocks the given 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 token must have the following permission set: `blocking:read`
///
///```no_run
/// async fn run() -> octocrab::Result<()> {
/// octocrab::instance()
/// .users("current_user")
/// .unblock_user("some_user")
/// .await
/// }
pub async fn unblock_user(&self, username: &str) -> crate::Result<()> {
let route = format!("/user/blocks/{}", username);

self.crab.delete(route, None::<&()>).await
}
}
57 changes: 57 additions & 0 deletions src/api/users/user_blocks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate::api::users::UserHandler;
use crate::models;

#[derive(serde::Serialize)]
pub struct BlockedUsersBuilder<'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> BlockedUsersBuilder<'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 users blocked by 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 token must have the following permission set: `blocking:read`
///
///```no_run
/// use octocrab::models::SimpleUser;
/// async fn run() -> octocrab::Result<Vec<SimpleUser>> {
/// let blocked_users = octocrab::instance()
/// .users("current_user")
/// .blocks()
/// .per_page(42).page(3u32)
/// .list()
/// .await?;
/// Ok(blocked_users.items)
/// }
pub async fn list(&self) -> crate::Result<crate::Page<models::SimpleUser>> {
let route = "/user/blocks".to_string();
self.handler.crab.get(route, None::<&()>).await
}
}
33 changes: 31 additions & 2 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use chrono::{DateTime, Utc};
use serde::{de, Deserialize, Deserializer, Serialize};
use url::Url;

pub use apps::App;

pub mod actions;
pub mod activity;
pub mod apps;
Expand All @@ -28,8 +30,6 @@ pub mod workflows;

mod date_serde;

pub use apps::App;

type BaseIdType = u64;

macro_rules! id_type {
Expand Down Expand Up @@ -478,6 +478,35 @@ pub struct UserProfile {
pub updated_at: DateTime<Utc>,
}

/// The simple profile for a GitHub user
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct SimpleUser {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
pub login: String,
pub id: UserId,
pub node_id: String,
pub avatar_url: Url,
pub gravatar_id: String,
pub url: Url,
pub html_url: Url,
pub followers_url: Url,
pub following_url: Url,
pub gists_url: Url,
pub starred_url: Url,
pub subscriptions_url: Url,
pub organizations_url: Url,
pub repos_url: Url,
pub events_url: Url,
pub received_events_url: Url,
pub r#type: String,
pub site_admin: bool,
pub starred_at: Option<DateTime<Utc>>,
}

/// A user that is following another user
#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
Expand Down
22 changes: 22 additions & 0 deletions tests/resources/user_blocks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[
{
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
}
]
119 changes: 119 additions & 0 deletions tests/user_blocks_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use serde::{Deserialize, Serialize};
use wiremock::{
matchers::{method, path},
Mock, MockServer, ResponseTemplate,
};

use mock_error::setup_error_handler;
use octocrab::models::SimpleUser;
use octocrab::Octocrab;

/// Tests API calls related to check runs of a specific commit.
mod mock_error;

#[derive(Serialize, Deserialize)]
struct FakePage<T> {
items: Vec<T>,
}

const NOT_BLOCKED: &str = "XAMPPRocky";

async fn setup_blocks_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!("GET 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_return_list_of_blocked_by_user() {
let mocked_response: Vec<SimpleUser> =
serde_json::from_str(include_str!("resources/user_blocks.json")).unwrap();
let template = ResponseTemplate::new(200).set_body_json(&mocked_response);
let mock_server = setup_blocks_mock("GET", "/user/blocks", template).await;
let client = setup_octocrab(&mock_server.uri());
let result = client.users("some-user").blocks().per_page(10).list().await;

assert!(
result.is_ok(),
"expected successful result, got error: {:#?}",
result
);

let response = result.unwrap();
let items = response.items;

assert_eq!(items.len(), 1);

{
let item = &items[0];

assert_eq!("octocat", item.login);
assert_eq!(
"https://api.github.com/users/octocat/received_events",
item.received_events_url.as_str()
);
}
}

#[tokio::test]
async fn should_check_if_user_blocked() {
/* status 204 for blocked */
let template = ResponseTemplate::new(200);
let mock_server = setup_blocks_mock(
"GET",
format!("/user/blocks/{}", NOT_BLOCKED).as_str(),
template,
)
.await;
let client = setup_octocrab(&mock_server.uri());
let result = client.users("some-user").is_blocked(NOT_BLOCKED).await;
assert!(!result.is_ok_and(|is_blocked| is_blocked));
}

#[tokio::test]
async fn should_respond_user_blocked() {
/* status 204 for blocked */
let template = ResponseTemplate::new(204);
let mock_server = setup_blocks_mock(
"PUT",
format!("/user/blocks/{}", NOT_BLOCKED).as_str(),
template,
)
.await;
let client = setup_octocrab(&mock_server.uri());
let result = client.users("some-user").block_user(NOT_BLOCKED).await;
assert!(result.is_ok());
}

#[tokio::test]
async fn should_respond_user_unblocked() {
/* status 204 for unblocked */
let template = ResponseTemplate::new(200);
let mock_server = setup_blocks_mock(
"DELETE",
format!("/user/blocks/{}", NOT_BLOCKED).as_str(),
template,
)
.await;
let client = setup_octocrab(&mock_server.uri());
let result = client.users("some-user").unblock_user(NOT_BLOCKED).await;
assert!(!result.is_ok());
}

0 comments on commit 736ccc1

Please sign in to comment.