From bcddbad84298ce2937454d45c41572ce88d1b9f4 Mon Sep 17 00:00:00 2001 From: Gerry Agbobada Date: Wed, 23 Aug 2023 12:23:23 +0200 Subject: [PATCH] Enhance installation webhook events - `new_permissions_accepted` events can now be deserialized even if they do not have the `repositories` field like the other actions of this webhook event type. - when the fully hydrated installation object is contained in a webhook event, octocrab now gives access to the `created_at` and `updated_at` timestamps related to the installation. This commit also mentions in the doc-string of Installation::events that at least in the webhook events case, it should be expected to have values matching the string versions of `octocrab::models::webhook_events::WebhookEventType` variants Fixes #444 --- src/models.rs | 17 ++++ src/models/webhook_events.rs | 37 ++++++++- .../webhook_events/payload/installation.rs | 3 +- ...ew_permissions_accepted_webhook_event.json | 82 +++++++++++++++++++ 4 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 tests/resources/installation_new_permissions_accepted_webhook_event.json diff --git a/src/models.rs b/src/models.rs index b6539b4a..0a9e4b6c 100644 --- a/src/models.rs +++ b/src/models.rs @@ -853,11 +853,28 @@ pub struct Installation { #[serde(skip_serializing_if = "Option::is_none")] pub target_type: Option, pub permissions: InstallationPermissions, + /// List of events in the installation. + /// + /// Note that for Webhook events, the list of events in the + /// list is guaranteed to match variants from + /// [WebhookEventType](webhook_events::WebhookEventType) pub events: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub single_file_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub repository_selection: Option, + #[serde( + skip_serializing_if = "Option::is_none", + default, + deserialize_with = "date_serde::deserialize_opt" + )] + pub created_at: Option>, + #[serde( + skip_serializing_if = "Option::is_none", + default, + deserialize_with = "date_serde::deserialize_opt" + )] + pub updated_at: Option>, } #[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)] diff --git a/src/models/webhook_events.rs b/src/models/webhook_events.rs index e257a44c..b1f5a35d 100644 --- a/src/models/webhook_events.rs +++ b/src/models/webhook_events.rs @@ -981,6 +981,8 @@ pub struct InstallationEventRepository { #[cfg(test)] mod tests { + use chrono::TimeZone; + use super::payload::*; use super::*; @@ -993,8 +995,8 @@ mod tests { install_event.action, InstallationWebhookEventAction::Created ); - assert_eq!(install_event.repositories.len(), 3); - assert_eq!(install_event.repositories[2].name, "octocrab"); + assert_eq!(install_event.repositories.as_ref().unwrap().len(), 3); + assert_eq!(install_event.repositories.unwrap()[2].name, "octocrab"); } #[test] @@ -1006,8 +1008,35 @@ mod tests { install_event.action, InstallationWebhookEventAction::Deleted ); - assert_eq!(install_event.repositories.len(), 3); - assert_eq!(install_event.repositories[2].name, "octocrab"); + assert_eq!(install_event.repositories.as_ref().unwrap().len(), 3); + assert_eq!(install_event.repositories.unwrap()[2].name, "octocrab"); + } + + #[test] + fn deserialize_installation_new_permissions_accepted() { + let json = include_str!( + "../../tests/resources/installation_new_permissions_accepted_webhook_event.json" + ); + let event = WebhookEvent::try_from_header_and_body("installation", json).unwrap(); + let WebhookEventPayload::Installation(ref install_event) = event.specific else {panic!(" event is of the wrong type {:?}", event)}; + let Some(EventInstallation::Full(installation)) = event.installation else {panic!("event is missing a fully described installation object {:?}", event)}; + assert_eq!( + install_event.action, + InstallationWebhookEventAction::NewPermissionsAccepted + ); + assert_eq!( + installation.updated_at.unwrap(), + chrono::Utc + .with_ymd_and_hms(2023, 8, 18, 13, 28, 4) + .unwrap() + ); + assert_eq!( + installation.created_at.unwrap(), + chrono::Utc + .with_ymd_and_hms(2023, 7, 13, 9, 35, 31) + .unwrap() + ); + assert_eq!(installation.events.len(), 12); } #[test] diff --git a/src/models/webhook_events/payload/installation.rs b/src/models/webhook_events/payload/installation.rs index af3e4159..014fb75d 100644 --- a/src/models/webhook_events/payload/installation.rs +++ b/src/models/webhook_events/payload/installation.rs @@ -7,7 +7,8 @@ use crate::models::webhook_events::InstallationEventRepository; pub struct InstallationWebhookEventPayload { pub action: InstallationWebhookEventAction, pub enterprise: Option, - pub repositories: Vec, + #[serde(default)] + pub repositories: Option>, pub requester: Option, } diff --git a/tests/resources/installation_new_permissions_accepted_webhook_event.json b/tests/resources/installation_new_permissions_accepted_webhook_event.json new file mode 100644 index 00000000..d501ce1e --- /dev/null +++ b/tests/resources/installation_new_permissions_accepted_webhook_event.json @@ -0,0 +1,82 @@ +{ + "action": "new_permissions_accepted", + "installation": { + "id": 88888888, + "account": { + "login": "gagbo", + "id": 10496163, + "node_id": "MDQ6VXNlcjEwNDk2MTYz", + "avatar_url": "https://avatars.githubusercontent.com/u/10496163?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/gagbo", + "html_url": "https://github.com/gagbo", + "followers_url": "https://api.github.com/users/gagbo/followers", + "following_url": "https://api.github.com/users/gagbo/following{/other_user}", + "gists_url": "https://api.github.com/users/gagbo/gists{/gist_id}", + "starred_url": "https://api.github.com/users/gagbo/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/gagbo/subscriptions", + "organizations_url": "https://api.github.com/users/gagbo/orgs", + "repos_url": "https://api.github.com/users/gagbo/repos", + "events_url": "https://api.github.com/users/gagbo/events{/privacy}", + "received_events_url": "https://api.github.com/users/gagbo/received_events", + "type": "User", + "site_admin": false + }, + "repository_selection": "all", + "access_tokens_url": "https://api.github.com/app/installations/88888888/access_tokens", + "repositories_url": "https://api.github.com/installation/repositories", + "html_url": "https://github.com/settings/installations/88888888", + "app_id": 7777777, + "app_slug": "gagbo-test-app", + "target_id": 10496163, + "target_type": "User", + "permissions": { + "issues": "write", + "actions": "write", + "contents": "read", + "metadata": "read", + "pull_requests": "write" + }, + "events": [ + "create", + "delete", + "fork", + "issues", + "issue_comment", + "pull_request", + "pull_request_review", + "pull_request_review_comment", + "pull_request_review_thread", + "push", + "release", + "repository" + ], + "created_at": "2023-07-13T11:35:31.000+02:00", + "updated_at": "2023-08-18T15:28:04.000+02:00", + "single_file_name": null, + "has_multiple_single_files": false, + "single_file_paths": [], + "suspended_by": null, + "suspended_at": null + }, + "sender": { + "login": "gagbo", + "id": 10496163, + "node_id": "MDQ6VXNlcjEwNDk2MTYz", + "avatar_url": "https://avatars.githubusercontent.com/u/10496163?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/gagbo", + "html_url": "https://github.com/gagbo", + "followers_url": "https://api.github.com/users/gagbo/followers", + "following_url": "https://api.github.com/users/gagbo/following{/other_user}", + "gists_url": "https://api.github.com/users/gagbo/gists{/gist_id}", + "starred_url": "https://api.github.com/users/gagbo/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/gagbo/subscriptions", + "organizations_url": "https://api.github.com/users/gagbo/orgs", + "repos_url": "https://api.github.com/users/gagbo/repos", + "events_url": "https://api.github.com/users/gagbo/events{/privacy}", + "received_events_url": "https://api.github.com/users/gagbo/received_events", + "type": "User", + "site_admin": false + } +}