From 5dc2980583f8d7a8ae0b328fbcb441b4618be5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFs=20Postula?= Date: Thu, 25 Jul 2024 09:40:48 +0200 Subject: [PATCH 1/2] feat: implement hook deliveries --- src/api.rs | 1 + src/api/hooks.rs | 69 ++++++++++++++++++++++++++++++ src/api/hooks/list_deliveries.rs | 55 ++++++++++++++++++++++++ src/api/hooks/retry_delivery.rs | 73 ++++++++++++++++++++++++++++++++ src/lib.rs | 7 ++- src/models.rs | 1 + src/models/hooks.rs | 15 +++++++ 7 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 src/api/hooks.rs create mode 100644 src/api/hooks/list_deliveries.rs create mode 100644 src/api/hooks/retry_delivery.rs diff --git a/src/api.rs b/src/api.rs index 7c21b00f..01e9e530 100644 --- a/src/api.rs +++ b/src/api.rs @@ -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; diff --git a/src/api/hooks.rs b/src/api/hooks.rs new file mode 100644 index 00000000..5ae6ba84 --- /dev/null +++ b/src/api/hooks.rs @@ -0,0 +1,69 @@ +//! The hooks API. +use crate::{Octocrab}; +use crate::models::{HookDeliveryId, HookId}; + +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, +} + +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) + } +} diff --git a/src/api/hooks/list_deliveries.rs b/src/api/hooks/list_deliveries.rs new file mode 100644 index 00000000..3c2d63d0 --- /dev/null +++ b/src/api/hooks/list_deliveries.rs @@ -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, + #[serde(skip_serializing_if = "Option::is_none")] + page: Option, +} +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) -> 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 + } + + /// Send the actual request. + pub async fn send(self) -> crate::Result> { + 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 + } +} diff --git a/src/api/hooks/retry_delivery.rs b/src/api/hooks/retry_delivery.rs new file mode 100644 index 00000000..1d8b53de --- /dev/null +++ b/src/api/hooks/retry_delivery.rs @@ -0,0 +1,73 @@ +use http::Uri; +use snafu::ResultExt; +use crate::error::HttpSnafu; +use super::*; + +/// 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) + + } +} + +#[cfg(test)] +mod tests { + // use crate::models::HookId; + + // #[tokio::test] + // async fn serialize() { + // let octocrab = crate::Octocrab::default(); + // let handler = octocrab.hooks("rust-lang"); + // let list = handler + // .list_deliveries(HookId::from(21u64)) + // .per_page(100) + // .page(1u8); + // + // assert_eq!( + // serde_json::to_value(list).unwrap(), + // serde_json::json!({ + // "state": "open", + // "head": "master", + // "base": "branch", + // "sort": "popularity", + // "direction": "asc", + // "per_page": 100, + // "page": 1, + // }) + // ) + // } +} diff --git a/src/lib.rs b/src/lib.rs index 8051cc17..907349e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -250,7 +250,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}, @@ -1146,6 +1146,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) -> hooks::HooksHandler { + hooks::HooksHandler::new(self, owner.into()) + } } /// # GraphQL API. diff --git a/src/models.rs b/src/models.rs index 4cddd571..bca2e3f6 100644 --- a/src/models.rs +++ b/src/models.rs @@ -111,6 +111,7 @@ id_type!( IssueId, JobId, HookId, + HookDeliveryId, LabelId, MilestoneId, NotificationId, diff --git a/src/models/hooks.rs b/src/models/hooks.rs index 5c97b8d2..30bb407a 100644 --- a/src/models/hooks.rs +++ b/src/models/hooks.rs @@ -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, + pub duration: f64, + pub status: String, + pub status_code: usize, + pub event: Option, + pub action: Option, + pub installation_id: Option, + pub repository_id: Option, +} From 7d3a582f46d9b8b46a8fb4fb601f28a95e35e41a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFs=20Postula?= Date: Thu, 25 Jul 2024 14:06:47 +0200 Subject: [PATCH 2/2] feat: fmt and move test to mocked test folder --- src/api/hooks.rs | 13 ++-- src/api/hooks/retry_delivery.rs | 53 +++++--------- tests/hooks_delivery_list.rs | 89 ++++++++++++++++++++++++ tests/resources/hooks_delivery_list.json | 32 +++++++++ 4 files changed, 145 insertions(+), 42 deletions(-) create mode 100644 tests/hooks_delivery_list.rs create mode 100644 tests/resources/hooks_delivery_list.json diff --git a/src/api/hooks.rs b/src/api/hooks.rs index 5ae6ba84..d849d765 100644 --- a/src/api/hooks.rs +++ b/src/api/hooks.rs @@ -1,14 +1,11 @@ //! The hooks API. -use crate::{Octocrab}; use crate::models::{HookDeliveryId, HookId}; +use crate::Octocrab; mod list_deliveries; mod retry_delivery; -pub use self::{ - list_deliveries::ListHooksDeliveriesBuilder, - retry_delivery::RetryDeliveryBuilder, -}; +pub use self::{list_deliveries::ListHooksDeliveriesBuilder, retry_delivery::RetryDeliveryBuilder}; /// A client to GitHub's webhooks API. /// @@ -63,7 +60,11 @@ impl<'octo> HooksHandler<'octo> { /// # Ok(()) /// # } /// ``` - pub fn retry_delivery(&self, hook_id: HookId, delivery_id: HookDeliveryId) -> RetryDeliveryBuilder<'_, '_> { + pub fn retry_delivery( + &self, + hook_id: HookId, + delivery_id: HookDeliveryId, + ) -> RetryDeliveryBuilder<'_, '_> { RetryDeliveryBuilder::new(self, hook_id, delivery_id) } } diff --git a/src/api/hooks/retry_delivery.rs b/src/api/hooks/retry_delivery.rs index 1d8b53de..760403cf 100644 --- a/src/api/hooks/retry_delivery.rs +++ b/src/api/hooks/retry_delivery.rs @@ -1,7 +1,7 @@ +use super::*; +use crate::error::HttpSnafu; use http::Uri; use snafu::ResultExt; -use crate::error::HttpSnafu; -use super::*; /// A builder pattern struct for listing hooks deliveries. /// @@ -18,19 +18,29 @@ pub struct RetryDeliveryBuilder<'octo, 'r> { delivery_id: HookDeliveryId, } impl<'octo, 'r> RetryDeliveryBuilder<'octo, 'r> { - pub(crate) fn new(handler: &'r HooksHandler<'octo>, hook_id: HookId, delivery_id: HookDeliveryId) -> Self { + pub(crate) fn new( + handler: &'r HooksHandler<'octo>, + hook_id: HookId, + delivery_id: HookDeliveryId, + ) -> Self { Self { handler, hook_id, - delivery_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 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() @@ -40,34 +50,5 @@ impl<'octo, 'r> RetryDeliveryBuilder<'octo, 'r> { crate::map_github_error(self.handler.crab._post(uri, None::<&()>).await?) .await .map(drop) - } } - -#[cfg(test)] -mod tests { - // use crate::models::HookId; - - // #[tokio::test] - // async fn serialize() { - // let octocrab = crate::Octocrab::default(); - // let handler = octocrab.hooks("rust-lang"); - // let list = handler - // .list_deliveries(HookId::from(21u64)) - // .per_page(100) - // .page(1u8); - // - // assert_eq!( - // serde_json::to_value(list).unwrap(), - // serde_json::json!({ - // "state": "open", - // "head": "master", - // "base": "branch", - // "sort": "popularity", - // "direction": "asc", - // "per_page": 100, - // "page": 1, - // }) - // ) - // } -} diff --git a/tests/hooks_delivery_list.rs b/tests/hooks_delivery_list.rs new file mode 100644 index 00000000..08243691 --- /dev/null +++ b/tests/hooks_delivery_list.rs @@ -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 { + items: Vec, +} + +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 = + 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), + } +} diff --git a/tests/resources/hooks_delivery_list.json b/tests/resources/hooks_delivery_list.json new file mode 100644 index 00000000..1bde6b20 --- /dev/null +++ b/tests/resources/hooks_delivery_list.json @@ -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 + } +]