Skip to content

Commit

Permalink
feat: implement hook deliveries (#668)
Browse files Browse the repository at this point in the history
  • Loading branch information
loispostula authored Jul 26, 2024
1 parent 6ef99b8 commit 4162be4
Show file tree
Hide file tree
Showing 9 changed files with 323 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod current;
pub mod events;
pub mod gists;
pub mod gitignore;
pub mod hooks;
pub mod issues;
pub mod licenses;
pub mod markdown;
Expand Down
70 changes: 70 additions & 0 deletions src/api/hooks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! The hooks API.
use crate::models::{HookDeliveryId, HookId};
use crate::Octocrab;

mod list_deliveries;
mod retry_delivery;

pub use self::{list_deliveries::ListHooksDeliveriesBuilder, retry_delivery::RetryDeliveryBuilder};

/// A client to GitHub's webhooks API.
///
/// Created with [`Octocrab::hooks`].
pub struct HooksHandler<'octo> {
crab: &'octo Octocrab,
owner: String,
repo: Option<String>,
}

impl<'octo> HooksHandler<'octo> {
pub(crate) fn new(crab: &'octo Octocrab, owner: String) -> Self {
Self {
crab,
owner,
repo: None,
}
}

pub fn repo(mut self, repo: String) -> Self {
self.repo = Some(repo);
self
}

/// Lists all of the `Delivery`s associated with the hook.
/// ```no_run
/// # async fn run() -> octocrab::Result<()> {
/// let reviews = octocrab::instance()
/// .hooks("owner")
/// //.repo("repo")
/// .list_deliveries(21u64.into())
/// .per_page(100)
/// .page(2u32)
/// .send()
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn list_deliveries(&self, hook_id: HookId) -> ListHooksDeliveriesBuilder<'_, '_> {
ListHooksDeliveriesBuilder::new(self, hook_id)
}

/// Retry a delivery.
/// ```no_run
/// # async fn run() -> octocrab::Result<()> {
/// let reviews = octocrab::instance()
/// .hooks("owner")
/// //.repo("repo")
/// .retry_delivery(20u64.into(), 21u64.into())
/// .send()
/// .await?;
/// # Ok(())
/// # }
/// ```
pub fn retry_delivery(
&self,
hook_id: HookId,
delivery_id: HookDeliveryId,
) -> RetryDeliveryBuilder<'_, '_> {
RetryDeliveryBuilder::new(self, hook_id, delivery_id)
}
}
55 changes: 55 additions & 0 deletions src/api/hooks/list_deliveries.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use super::*;

/// A builder pattern struct for listing hooks deliveries.
///
/// created by [`HooksHandler::list_deliveries`]
///
/// [`HooksHandler::list_deliveries`]: ./struct.HooksHandler.html#method.list_deliveries
#[derive(serde::Serialize)]
pub struct ListHooksDeliveriesBuilder<'octo, 'r> {
#[serde(skip)]
handler: &'r HooksHandler<'octo>,
#[serde(skip)]
hook_id: HookId,
#[serde(skip_serializing_if = "Option::is_none")]
per_page: Option<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
page: Option<u32>,
}
impl<'octo, 'r> ListHooksDeliveriesBuilder<'octo, 'r> {
pub(crate) fn new(handler: &'r HooksHandler<'octo>, hook_id: HookId) -> Self {
Self {
handler,
hook_id,
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
}

/// Send the actual request.
pub async fn send(self) -> crate::Result<crate::Page<crate::models::hooks::Delivery>> {
let route = match self.handler.repo.clone() {
Some(repo) => format!(
"/repos/{}/{}/hooks/{}/deliveries",
self.handler.owner, repo, self.hook_id
),
None => format!(
"/orgs/{}/hooks/{}/deliveries",
self.handler.owner, self.hook_id
),
};
self.handler.crab.get(route, Some(&self)).await
}
}
54 changes: 54 additions & 0 deletions src/api/hooks/retry_delivery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use super::*;
use crate::error::HttpSnafu;
use http::Uri;
use snafu::ResultExt;

/// A builder pattern struct for listing hooks deliveries.
///
/// created by [`HooksHandler::retry_delivery`]
///
/// [`HooksHandler::retry_delivery`]: ./struct.HooksHandler.html#method.retry_delivery
#[derive(serde::Serialize)]
pub struct RetryDeliveryBuilder<'octo, 'r> {
#[serde(skip)]
handler: &'r HooksHandler<'octo>,
#[serde(skip)]
hook_id: HookId,
#[serde(skip)]
delivery_id: HookDeliveryId,
}
impl<'octo, 'r> RetryDeliveryBuilder<'octo, 'r> {
pub(crate) fn new(
handler: &'r HooksHandler<'octo>,
hook_id: HookId,
delivery_id: HookDeliveryId,
) -> Self {
Self {
handler,
hook_id,
delivery_id,
}
}

/// Send the actual request.
pub async fn send(self) -> crate::Result<()> {
let route = match self.handler.repo.clone() {
Some(repo) => format!(
"/repos/{}/{}/hooks/{}/deliveries/{}/attempts",
self.handler.owner, repo, self.hook_id, self.delivery_id
),
None => format!(
"/orgs/{}/hooks/{}/deliveries/{}/attempts",
self.handler.owner, self.hook_id, self.delivery_id
),
};

let uri = Uri::builder()
.path_and_query(route)
.build()
.context(HttpSnafu)?;
crate::map_github_error(self.handler.crab._post(uri, None::<&()>).await?)
.await
.map(drop)
}
}
7 changes: 6 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ use models::{AppId, InstallationId, InstallationToken};

pub use self::{
api::{
actions, activity, apps, checks, commits, current, events, gists, gitignore, issues,
actions, activity, apps, checks, commits, current, events, gists, gitignore, hooks, issues,
licenses, markdown, orgs, projects, pulls, ratelimit, repos, search, teams, workflows,
},
error::{Error, GitHubError},
Expand Down Expand Up @@ -1148,6 +1148,11 @@ impl Octocrab {
pub fn ratelimit(&self) -> ratelimit::RateLimitHandler {
ratelimit::RateLimitHandler::new(self)
}

/// Creates a [`hooks::HooksHandler`] that returns the API hooks
pub fn hooks(&self, owner: impl Into<String>) -> hooks::HooksHandler {
hooks::HooksHandler::new(self, owner.into())
}
}

/// # GraphQL API.
Expand Down
1 change: 1 addition & 0 deletions src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ id_type!(
IssueId,
JobId,
HookId,
HookDeliveryId,
LabelId,
MilestoneId,
NotificationId,
Expand Down
15 changes: 15 additions & 0 deletions src/models/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,18 @@ pub enum ContentType {
#[serde(untagged)]
Other(String),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Delivery {
pub id: HookDeliveryId,
pub guid: String,
pub delivered_at: DateTime<Utc>,
pub duration: f64,
pub status: String,
pub status_code: usize,
pub event: Option<WebhookEventType>,
pub action: Option<String>,
pub installation_id: Option<InstallationId>,
pub repository_id: Option<InstallationId>,
}
89 changes: 89 additions & 0 deletions tests/hooks_delivery_list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/// Tests API calls related to check runs of a specific commit.
mod mock_error;

use mock_error::setup_error_handler;
use octocrab::models::hooks::Delivery;
use octocrab::models::HookId;
use octocrab::{Error, Octocrab};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use wiremock::{
matchers::{method, path},
Mock, MockServer, ResponseTemplate,
};

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

const OWNER: &str = "XAMPPRocky";

async fn setup_get_api(template: ResponseTemplate, number: u64) -> MockServer {
let mock_server = MockServer::start().await;

Mock::given(method("GET"))
.and(path(format!("/orgs/{OWNER}/hooks/{number}/deliveries")))
.respond_with(template)
.mount(&mock_server)
.await;
setup_error_handler(
&mock_server,
&format!("GET on /orgs/{OWNER}/hooks/{number}/deliveries 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_deliveries_for_org_by_id() {
let number: u64 = 148681297;
let mocked_response: Vec<Delivery> =
serde_json::from_str(include_str!("resources/hooks_delivery_list.json")).unwrap();
let template = ResponseTemplate::new(200).set_body_json(&mocked_response);
let mock_server = setup_get_api(template, number).await;
let client = setup_octocrab(&mock_server.uri());
let result = client
.hooks(OWNER)
.list_deliveries(HookId(number))
.send()
.await;

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

let hooks = result.unwrap().items;
assert_eq!(hooks.len(), 2);
}

#[tokio::test]
async fn should_fail_when_no_deliveries_found() {
let mocked_response = json!({
"documentation_url": json!("rtm"),
"errors": Value::Null,
"message": json!("Its gone")
});

let template = ResponseTemplate::new(404).set_body_json(&mocked_response);
let mock_server = setup_get_api(template, 404).await;
let client = setup_octocrab(&mock_server.uri());
let result = client
.hooks(OWNER)
.list_deliveries(HookId(404))
.send()
.await;

match result.unwrap_err() {
Error::GitHub { source, .. } => {
assert_eq!("Its gone", source.message)
}
other => panic!("Unexpected error: {:?}", other),
}
}
32 changes: 32 additions & 0 deletions tests/resources/hooks_delivery_list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[
{
"id": 93676014012,
"guid": "180a8f00-4a7c-11ef-8350-d1961bccd09f",
"delivered_at": "2024-07-25T11:50:32Z",
"redelivery": false,
"duration": 0.32,
"status": "Invalid HTTP Response: 503",
"status_code": 503,
"event": "workflow_job",
"action": "completed",
"installation_id": null,
"repository_id": 1,
"url": "",
"throttled_at": null
},
{
"id": 93676002432,
"guid": "14d5f0e0-4a7c-11ef-8465-fadda1832ea4",
"delivered_at": "2024-07-25T11:50:26Z",
"redelivery": false,
"duration": 0.4,
"status": "OK",
"status_code": 200,
"event": "workflow_job",
"action": "in_progress",
"installation_id": null,
"repository_id": 1,
"url": "",
"throttled_at": null
}
]

0 comments on commit 4162be4

Please sign in to comment.