use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::client::Client;
use crate::errors::Error;
use crate::KeygenResponseData;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum WebhookEventStatus {
#[serde(rename = "DELIVERING")]
Delivering,
#[serde(rename = "DELIVERED")]
Delivered,
#[serde(rename = "FAILING")]
Failing,
#[serde(rename = "FAILED")]
Failed,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct WebhookEventAttributes {
pub endpoint: String,
pub payload: Value,
pub event: String,
pub status: WebhookEventStatus,
#[serde(rename = "lastResponseCode")]
pub last_response_code: Option<i32>,
#[serde(rename = "lastResponseBody")]
pub last_response_body: Option<String>,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct WebhookEventResponse {
pub data: KeygenResponseData<WebhookEventAttributes>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct WebhookEventsResponse {
pub data: Vec<KeygenResponseData<WebhookEventAttributes>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebhookEventRecord {
pub id: String,
pub endpoint: String,
pub payload: Value,
pub event: String,
pub status: WebhookEventStatus,
pub last_response_code: Option<i32>,
pub last_response_body: Option<String>,
pub created: DateTime<Utc>,
pub updated: DateTime<Utc>,
pub account_id: Option<String>,
}
#[derive(Debug, Default)]
pub struct WebhookEventListOptions {
pub limit: Option<i32>,
pub page_number: Option<i32>,
pub page_size: Option<i32>,
pub event_type: Option<String>,
pub status: Option<WebhookEventStatus>,
}
impl WebhookEventRecord {
pub(crate) fn from(data: KeygenResponseData<WebhookEventAttributes>) -> Self {
WebhookEventRecord {
id: data.id,
endpoint: data.attributes.endpoint,
payload: data.attributes.payload,
event: data.attributes.event,
status: data.attributes.status,
last_response_code: data.attributes.last_response_code,
last_response_body: data.attributes.last_response_body,
created: data.attributes.created,
updated: data.attributes.updated,
account_id: data
.relationships
.account
.as_ref()
.and_then(|a| a.data.as_ref().map(|d| d.id.clone())),
}
}
#[cfg(feature = "token")]
pub async fn list(
options: Option<&WebhookEventListOptions>,
) -> Result<Vec<WebhookEventRecord>, Error> {
let client = Client::from_global_config()?;
let mut query = serde_json::json!({});
if let Some(opts) = options {
if let Some(limit) = opts.limit {
query["limit"] = serde_json::json!(limit);
}
if let Some(page_number) = opts.page_number {
query["page[number]"] = serde_json::json!(page_number);
}
if let Some(page_size) = opts.page_size {
query["page[size]"] = serde_json::json!(page_size);
}
if let Some(ref event_type) = opts.event_type {
query["event"] = serde_json::json!(event_type);
}
if let Some(ref status) = opts.status {
query["status"] = serde_json::to_value(status).unwrap_or_default();
}
}
let response = client.get("webhook-events", Some(&query)).await?;
let events_response: WebhookEventsResponse = serde_json::from_value(response.body)?;
Ok(events_response
.data
.into_iter()
.map(WebhookEventRecord::from)
.collect())
}
#[cfg(feature = "token")]
pub async fn get(id: &str) -> Result<WebhookEventRecord, Error> {
let client = Client::from_global_config()?;
let endpoint = format!("webhook-events/{id}");
let response = client.get(&endpoint, None::<&()>).await?;
let event_response: WebhookEventResponse = serde_json::from_value(response.body)?;
Ok(WebhookEventRecord::from(event_response.data))
}
#[cfg(feature = "token")]
pub async fn retry(&self) -> Result<WebhookEventRecord, Error> {
let client = Client::from_global_config()?;
let endpoint = format!("webhook-events/{}/actions/retry", self.id);
let response = client.post(&endpoint, None::<&()>, None::<&()>).await?;
let event_response: WebhookEventResponse = serde_json::from_value(response.body)?;
Ok(WebhookEventRecord::from(event_response.data))
}
#[cfg(feature = "token")]
pub async fn delete(&self) -> Result<(), Error> {
let client = Client::from_global_config()?;
let endpoint = format!("webhook-events/{}", self.id);
client.delete::<(), ()>(&endpoint, None::<&()>).await?;
Ok(())
}
pub fn is_delivered(&self) -> bool {
matches!(self.status, WebhookEventStatus::Delivered)
}
pub fn is_failed(&self) -> bool {
matches!(self.status, WebhookEventStatus::Failed)
}
pub fn is_pending(&self) -> bool {
matches!(
self.status,
WebhookEventStatus::Delivering | WebhookEventStatus::Failing
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{reset_config, set_config, KeygenConfig};
use mockito::{mock, server_url};
use serde_json::json;
#[cfg(feature = "token")]
#[tokio::test]
async fn test_list_webhook_events() {
let _m = mock("GET", "/v1/webhook-events")
.match_header("authorization", "Bearer admin-token")
.match_query(mockito::Matcher::Any)
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": [
{
"id": "event-1",
"type": "webhook-events",
"meta": {
"idempotencyToken": "token-123"
},
"attributes": {
"endpoint": "https://example.com/webhook",
"payload": {"test": "data"},
"event": "license.created",
"status": "DELIVERED",
"lastResponseCode": 200,
"lastResponseBody": "OK",
"created": "2024-01-01T00:00:00Z",
"updated": "2024-01-01T00:00:01Z"
},
"relationships": {
"account": {
"data": {
"type": "accounts",
"id": "account-123"
}
}
}
}
]
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let result = WebhookEventRecord::list(None).await;
assert!(result.is_ok());
let events = result.unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].id, "event-1");
assert_eq!(events[0].event, "license.created");
assert!(events[0].is_delivered());
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_get_webhook_event() {
let _m = mock("GET", "/v1/webhook-events/event-123")
.match_header("authorization", "Bearer admin-token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": {
"id": "event-123",
"type": "webhook-events",
"attributes": {
"endpoint": "https://example.com/webhook",
"payload": {"license": {"id": "license-456"}},
"event": "license.validation.succeeded",
"status": "FAILED",
"lastResponseCode": 500,
"lastResponseBody": "Internal Server Error",
"created": "2024-01-01T00:00:00Z",
"updated": "2024-01-01T00:00:10Z"
},
"relationships": {}
}
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let result = WebhookEventRecord::get("event-123").await;
assert!(result.is_ok());
let event = result.unwrap();
assert_eq!(event.id, "event-123");
assert_eq!(event.event, "license.validation.succeeded");
assert!(event.is_failed());
assert_eq!(event.last_response_code, Some(500));
let _ = reset_config();
}
#[cfg(feature = "token")]
#[tokio::test]
async fn test_retry_webhook_event() {
let event = WebhookEventRecord {
id: "event-123".to_string(),
endpoint: "https://example.com/webhook".to_string(),
payload: json!({"test": "data"}),
event: "license.created".to_string(),
status: WebhookEventStatus::Failed,
last_response_code: Some(500),
last_response_body: Some("Error".to_string()),
created: Utc::now(),
updated: Utc::now(),
account_id: None,
};
let _m = mock("POST", "/v1/webhook-events/event-123/actions/retry")
.match_header("authorization", "Bearer admin-token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
json!({
"data": {
"id": "event-123",
"type": "webhook-events",
"attributes": {
"endpoint": "https://example.com/webhook",
"payload": {"test": "data"},
"event": "license.created",
"status": "DELIVERING",
"lastResponseCode": null,
"lastResponseBody": null,
"created": "2024-01-01T00:00:00Z",
"updated": "2024-01-01T00:00:15Z"
},
"relationships": {}
}
})
.to_string(),
)
.create();
let _ = set_config(KeygenConfig {
api_url: server_url(),
account: "test_account".to_string(),
token: Some("admin-token".to_string()),
..Default::default()
});
let result = event.retry().await;
assert!(result.is_ok());
let retried = result.unwrap();
assert!(retried.is_pending());
assert_eq!(retried.last_response_code, None);
let _ = reset_config();
}
#[test]
fn test_webhook_event_status_checks() {
let delivered_event = WebhookEventRecord {
id: "1".to_string(),
endpoint: "https://example.com".to_string(),
payload: json!({}),
event: "test".to_string(),
status: WebhookEventStatus::Delivered,
last_response_code: Some(200),
last_response_body: None,
created: Utc::now(),
updated: Utc::now(),
account_id: None,
};
assert!(delivered_event.is_delivered());
assert!(!delivered_event.is_failed());
assert!(!delivered_event.is_pending());
let failed_event = WebhookEventRecord {
status: WebhookEventStatus::Failed,
..delivered_event.clone()
};
assert!(!failed_event.is_delivered());
assert!(failed_event.is_failed());
assert!(!failed_event.is_pending());
let delivering_event = WebhookEventRecord {
status: WebhookEventStatus::Delivering,
..delivered_event
};
assert!(!delivering_event.is_delivered());
assert!(!delivering_event.is_failed());
assert!(delivering_event.is_pending());
}
}