Skip to content

Commit

Permalink
feat: added ssh_signing_keys ops (#725)
Browse files Browse the repository at this point in the history
  • Loading branch information
dmgorsky authored Oct 16, 2024
1 parent c043d06 commit 3958f28
Show file tree
Hide file tree
Showing 6 changed files with 329 additions and 0 deletions.
11 changes: 11 additions & 0 deletions src/api/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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::api::users::user_ssh_signing_keys::UserSshSigningKeysOpsBuilder;
use crate::models::UserId;
use crate::params::users::emails::EmailVisibilityState;
use crate::{error, GitHubError, Octocrab};
Expand All @@ -23,6 +24,7 @@ mod user_git_ssh_keys;
mod user_gpg_keys;
mod user_repos;
mod user_social_accounts;
mod user_ssh_signing_keys;

pub(crate) enum UserRef {
ByString(String),
Expand Down Expand Up @@ -212,4 +214,13 @@ impl<'octo> UserHandler<'octo> {
pub fn social_accounts(&self) -> UserSocialAccountsOpsBuilder<'_, '_> {
UserSocialAccountsOpsBuilder::new(self)
}

///SSH signing key administration
///* List SSH signing keys for the authenticated user
///* Create an SSH signing key for the authenticated user
///* Get an SSH signing key for the authenticated user
///* Delete an SSH signing key for the authenticated user
pub fn ssh_signing_keys(&self) -> UserSshSigningKeysOpsBuilder<'_, '_> {
UserSshSigningKeysOpsBuilder::new(self)
}
}
152 changes: 152 additions & 0 deletions src/api/users/user_ssh_signing_keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
use crate::api::users::UserHandler;
use crate::models::SshSigningKey;
use crate::{FromResponse, Page};

#[derive(serde::Serialize)]
pub struct UserSshSigningKeysOpsBuilder<'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> UserSshSigningKeysOpsBuilder<'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 SSH signing keys 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)
///
///OAuth app tokens and personal access tokens (classic) need the `read:ssh_signing_key` scope
///
///The fine-grained token must have the following permission set:
///* "SSH signing keys" user permissions (read)
///
///```no_run
/// use octocrab::models::SshSigningKey;
/// use octocrab::{Page, Result};
/// async fn run() -> Result<Page<SshSigningKey>> {
/// octocrab::instance()
/// .users("current_user")
/// .ssh_signing_keys()
/// .per_page(42).page(3u32)
/// .list()
/// .await
/// }
pub async fn list(&self) -> crate::Result<Page<crate::models::SshSigningKey>> {
let route = "/user/ssh_signing_keys".to_string();
self.handler.crab.get(route, Some(&self)).await
}

///## Get extended details for an SSH signing key for the authenticated user
///
///OAuth app tokens and personal access tokens (classic) need the `read:ssh_signing_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:
///* "SSH signing keys" user permissions (read)
///
///```no_run
/// use octocrab::models::SshSigningKey;
/// use octocrab::Result;
/// async fn run() -> Result<SshSigningKey> {
/// octocrab::instance()
/// .users("current_user")
/// .ssh_signing_keys()
/// .get(42)
/// .await
/// }
pub async fn get(&self, ssh_signing_key_id: u64) -> crate::Result<SshSigningKey> {
let route = format!("/user/ssh_signing_keys/{ssh_signing_key_id}");
self.handler.crab.get(route, None::<&()>).await
}

///## Create a SSH signing key for the authenticated user
/// OAuth app tokens and personal access tokens (classic) need the `write:ssh_signing_key` scope to use this method.
///
///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:
///* "SSH signing keys" user permissions (write)
///
///```no_run
/// use octocrab::models::SshSigningKey;
/// use octocrab::Result;
/// async fn run() -> Result<SshSigningKey> {
/// octocrab::instance()
/// .users("current_user")
/// .ssh_signing_keys()
/// .add("ssh-rsa AAAAB3NzaC1yc2EAAA".to_string(), "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234".to_string())
/// .await
/// }
pub async fn add(&self, title: String, key: String) -> crate::Result<SshSigningKey> {
let route = "/user/ssh_signing_keys".to_string();

let params = serde_json::json!({
"title": title,
"key": key,
});
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());
}

<SshSigningKey>::from_response(crate::map_github_error(response).await?).await
}

///## Delete an SSH signing key for the authenticated user
/// OAuth app tokens and personal access tokens (classic) need the `admin:ssh_signing_key` 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:
///* "SSH signing keys" user permissions (write)
///
///```no_run
/// use octocrab::Result;
/// async fn run() -> Result<()> {
/// octocrab::instance()
/// .users("current_user")
/// .ssh_signing_keys()
/// .delete(42)
/// .await
/// }
pub async fn delete(&self, ssh_signing_key_id: u64) -> crate::Result<()> {
let route = format!("/user/ssh_signing_keys/{ssh_signing_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(())
}
}
8 changes: 8 additions & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1176,3 +1176,11 @@ pub struct SocialAccount {
pub provider: String,
pub url: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SshSigningKey {
pub key: String,
pub id: u64,
pub title: String,
pub created_at: DateTime<Utc>,
}
7 changes: 7 additions & 0 deletions tests/resources/user_ssh_signing_key_created.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"key": "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234",
"id": 2,
"url": "https://api.github.com/user/keys/2",
"title": "ssh-rsa AAAAB3NzaC1yc2EAAA",
"created_at": "2020-06-11T21:31:57Z"
}
16 changes: 16 additions & 0 deletions tests/resources/user_ssh_signing_keys.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[
{
"key": "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv1234",
"id": 2,
"url": "https://api.github.com/user/keys/2",
"title": "ssh-rsa AAAAB3NzaC1yc2EAAA",
"created_at": "2020-06-11T21:31:57Z"
},
{
"key": "2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJy931234",
"id": 3,
"url": "https://api.github.com/user/keys/3",
"title": "ssh-rsa AAAAB3NzaC1yc2EAAB",
"created_at": "2020-07-11T21:31:57Z"
}
]
135 changes: 135 additions & 0 deletions tests/user_ssh_signing_keys_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use http::StatusCode;
use wiremock::{
matchers::{method, path},
Mock, MockServer, ResponseTemplate,
};

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

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

const SSH_SIGNING_KEY_ID: u64 = 42;

async fn setup_ssh_signing_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_ssh_signing_key() {
let mocked_response: SshSigningKey =
serde_json::from_str(include_str!("resources/user_ssh_signing_key_created.json")).unwrap();
let template = ResponseTemplate::new(200).set_body_json(&mocked_response);
let mock_server = setup_ssh_signing_keys_mock(
"GET",
format!("/user/ssh_signing_keys/{SSH_SIGNING_KEY_ID}").as_str(),
template,
)
.await;
let client = setup_octocrab(&mock_server.uri());
let result = client
.users("some_other_user")
.ssh_signing_keys()
.get(SSH_SIGNING_KEY_ID)
.await;
assert!(
result.is_ok(),
"expected successful result, got error: {:#?}",
result
);
let response = result.unwrap();
let id = response.id;
assert_eq!(id, 2);
}

#[tokio::test]
async fn should_respond_to_ssh_signing_keys_list() {
let mocked_response: Vec<SshSigningKey> =
serde_json::from_str(include_str!("resources/user_ssh_signing_keys.json")).unwrap();
let template = ResponseTemplate::new(200).set_body_json(&mocked_response);
let mock_server = setup_ssh_signing_keys_mock("GET", "/user/ssh_signing_keys", template).await;
let client = setup_octocrab(&mock_server.uri());
let result = client
.users("some_other_user")
.ssh_signing_keys()
.per_page(42)
.page(3u32)
.list()
.await;
assert!(
result.is_ok(),
"expected successful result, got error: {:#?}",
result
);
let response = result.unwrap();
let id = response.items.first().unwrap().id;
assert_eq!(id, 2);
}

#[tokio::test]
async fn should_respond_to_ssh_signing_keys_add() {
let mocked_response: SshSigningKey =
serde_json::from_str(include_str!("resources/user_ssh_signing_key_created.json")).unwrap();
let template = ResponseTemplate::new(StatusCode::CREATED).set_body_json(&mocked_response);
let mock_server = setup_ssh_signing_keys_mock("POST", "/user/ssh_signing_keys", template).await;
let client = setup_octocrab(&mock_server.uri());
let result = client
.users("some_user")
.ssh_signing_keys()
.add(
"Assh-rsa AAAAB3NzaC1yc2EAA".to_string(),
"A2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvv123".to_string(),
)
.await;
assert!(
result.is_ok(),
"expected successful result, got error: {:#?}",
result
);
let result = result.unwrap();
assert_eq!(result.id, 2);
}

#[tokio::test]
async fn should_respond_to_ssh_signing_key_delete() {
let template = ResponseTemplate::new(StatusCode::NO_CONTENT);
let mock_server = setup_ssh_signing_keys_mock(
"DELETE",
format!("/user/ssh_signing_keys/{SSH_SIGNING_KEY_ID}").as_str(),
template,
)
.await;
let client = setup_octocrab(&mock_server.uri());
let result = client
.users("some_user")
.ssh_signing_keys()
.delete(SSH_SIGNING_KEY_ID)
.await;
assert!(
result.is_ok(),
"expected successful result, got error: {:#?}",
result
);
}

0 comments on commit 3958f28

Please sign in to comment.