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..d849d765 --- /dev/null +++ b/src/api/hooks.rs @@ -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, +} + +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..760403cf --- /dev/null +++ b/src/api/hooks/retry_delivery.rs @@ -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) + } +} 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, +} 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 + } +]