use chrono::Utc;
pub fn mock_checkout_completed_event(session_id: &str, customer_id: &str) -> String {
serde_json::json!({
"id": "evt_mock_checkout_completed",
"object": "event",
"api_version": "2023-10-16",
"created": Utc::now().timestamp(),
"livemode": false,
"pending_webhooks": 1,
"request": null,
"type": "checkout.session.completed",
"data": {
"object": {
"id": session_id,
"object": "checkout.session",
"customer": customer_id,
"payment_status": "paid",
"status": "complete",
"created": 1700000000_i64,
"expires_at": 1700086400_i64,
"livemode": false,
"mode": "payment",
"payment_method_types": ["card"],
"custom_fields": [],
"custom_text": {
"after_submit": null,
"shipping_address": null,
"submit": null,
"terms_of_service_acceptance": null
},
"shipping_options": [],
"automatic_tax": {
"enabled": false,
"status": null
}
}
}
})
.to_string()
}
pub fn mock_subscription_updated_event(
subscription_id: &str,
customer_id: &str,
status: &str,
) -> String {
serde_json::json!({
"id": "evt_mock_subscription_updated",
"object": "event",
"api_version": "2023-10-16",
"created": Utc::now().timestamp(),
"livemode": false,
"pending_webhooks": 1,
"request": null,
"type": "customer.subscription.updated",
"data": {
"object": {
"id": subscription_id,
"object": "subscription",
"customer": customer_id,
"status": status
}
}
})
.to_string()
}
pub fn mock_subscription_deleted_event(subscription_id: &str, customer_id: &str) -> String {
serde_json::json!({
"id": "evt_mock_subscription_deleted",
"object": "event",
"api_version": "2023-10-16",
"created": Utc::now().timestamp(),
"livemode": false,
"pending_webhooks": 1,
"request": null,
"type": "customer.subscription.deleted",
"data": {
"object": {
"id": subscription_id,
"object": "subscription",
"customer": customer_id,
"status": "canceled"
}
}
})
.to_string()
}
pub fn mock_invoice_paid_event(invoice_id: &str, customer_id: &str) -> String {
serde_json::json!({
"id": "evt_mock_invoice_paid",
"object": "event",
"api_version": "2023-10-16",
"created": Utc::now().timestamp(),
"livemode": false,
"pending_webhooks": 1,
"request": null,
"type": "invoice.paid",
"data": {
"object": {
"id": invoice_id,
"object": "invoice",
"customer": customer_id,
"status": "paid",
"amount_paid": 1000,
"currency": "usd"
}
}
})
.to_string()
}
pub fn signed_webhook_payload(payload: &str, secret: &str) -> (String, i64) {
use hmac::{Hmac, Mac};
use sha2::Sha256;
let timestamp = chrono::Utc::now().timestamp();
let signed_payload = format!("{timestamp}.{payload}");
let mut mac =
Hmac::<Sha256>::new_from_slice(secret.as_bytes()).expect("HMAC can take key of any size");
mac.update(signed_payload.as_bytes());
let result = mac.finalize();
let signature = hex::encode(result.into_bytes());
let header = format!("t={timestamp},v1={signature}");
(header, timestamp)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::verify_webhook;
#[test]
fn signed_webhook_payload_round_trips_through_verify_webhook() {
let payload = r#"{"id":"evt_test","object":"event","api_version":"2023-10-16","created":1533204620,"livemode":false,"pending_webhooks":1,"request":null,"type":"invoiceitem.created","data":{"object":{"id":"ii_123","object":"invoiceitem","amount":1000,"currency":"usd","customer":"cus_123","date":1533204620,"description":"Test","discountable":false,"invoice":"in_123","livemode":false,"metadata":{},"period":{"start":1533204620,"end":1533204620},"proration":false,"quantity":1}}}"#;
let secret = "whsec_test_round_trip_secret";
let (sig, _ts) = signed_webhook_payload(payload, secret);
let result = verify_webhook(payload, &sig, secret);
assert!(
result.is_ok(),
"signed_webhook_payload should produce a signature that verify_webhook accepts: {result:?}"
);
}
#[test]
fn mock_checkout_completed_event_has_correct_type_field() {
let event = mock_checkout_completed_event("cs_test_123", "cus_test_456");
let parsed: serde_json::Value = serde_json::from_str(&event).expect("should be valid JSON");
assert_eq!(parsed["type"], "checkout.session.completed");
assert_eq!(parsed["data"]["object"]["id"], "cs_test_123");
assert_eq!(parsed["data"]["object"]["customer"], "cus_test_456");
}
#[test]
fn mock_subscription_updated_event_has_correct_type_field() {
let event = mock_subscription_updated_event("sub_123", "cus_456", "active");
let parsed: serde_json::Value = serde_json::from_str(&event).expect("should be valid JSON");
assert_eq!(parsed["type"], "customer.subscription.updated");
assert_eq!(parsed["data"]["object"]["id"], "sub_123");
assert_eq!(parsed["data"]["object"]["customer"], "cus_456");
assert_eq!(parsed["data"]["object"]["status"], "active");
}
#[test]
fn mock_subscription_deleted_event_has_correct_type_field() {
let event = mock_subscription_deleted_event("sub_789", "cus_012");
let parsed: serde_json::Value = serde_json::from_str(&event).expect("should be valid JSON");
assert_eq!(parsed["type"], "customer.subscription.deleted");
assert_eq!(parsed["data"]["object"]["id"], "sub_789");
}
#[test]
fn mock_invoice_paid_event_has_correct_type_field() {
let event = mock_invoice_paid_event("in_test_123", "cus_test_456");
let parsed: serde_json::Value = serde_json::from_str(&event).expect("should be valid JSON");
assert_eq!(parsed["type"], "invoice.paid");
assert_eq!(parsed["data"]["object"]["id"], "in_test_123");
assert_eq!(parsed["data"]["object"]["customer"], "cus_test_456");
}
}