use std::fmt;
use crate::error::Error;
use crate::id::{EventId, PaymentId};
use crate::money::Money;
use crate::provider::{ProviderId, async_trait};
use crate::raw::Raw;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("`{name}` arrived {count} times; a signed delivery carries it once")]
pub struct RepeatedHeader {
pub name: Box<str>,
pub count: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct Delivery<'a> {
headers: &'a [(&'a str, &'a str)],
body: &'a [u8],
}
impl<'a> Delivery<'a> {
#[must_use]
pub const fn new(headers: &'a [(&'a str, &'a str)], body: &'a [u8]) -> Self {
Self { headers, body }
}
#[must_use]
pub fn header(&self, name: &str) -> Option<&'a str> {
self.headers
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))
.map(|(_, value)| *value)
}
pub fn signed_header(&self, name: &str) -> Result<Option<&'a str>, RepeatedHeader> {
let mut found = self
.headers
.iter()
.filter(|(key, _)| key.eq_ignore_ascii_case(name));
let Some((_, value)) = found.next() else {
return Ok(None);
};
let count = 1 + found.count();
if count > 1 {
return Err(RepeatedHeader {
name: name.into(),
count,
});
}
Ok(Some(value))
}
#[must_use]
pub const fn headers(&self) -> &'a [(&'a str, &'a str)] {
self.headers
}
#[must_use]
pub const fn body(&self) -> &'a [u8] {
self.body
}
#[must_use]
pub fn body_str(&self) -> Option<&'a str> {
std::str::from_utf8(self.body).ok()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum EventKind {
Authorized,
Captured,
Refunded,
Failed,
Canceled,
Other(Box<str>),
}
#[derive(Debug, Clone)]
pub struct Event {
pub id: EventId,
pub kind: EventKind,
pub payment: Option<PaymentId>,
pub amount: Option<Money>,
pub provider: ProviderId,
pub raw: Raw,
}
#[async_trait]
pub trait Webhook: fmt::Debug + Send + Sync {
fn provider(&self) -> ProviderId;
async fn verify(&self, delivery: &Delivery<'_>) -> Result<Event, Error>;
}
#[cfg(test)]
mod tests {
use super::Delivery;
#[test]
fn a_header_is_found_whatever_case_it_arrived_in() {
let headers = [("Stripe-Signature", "t=1,v1=abc"), ("Accept", "*/*")];
let delivery = Delivery::new(&headers, b"{}");
assert_eq!(delivery.header("stripe-signature"), Some("t=1,v1=abc"));
assert_eq!(delivery.header("STRIPE-SIGNATURE"), Some("t=1,v1=abc"));
assert_eq!(delivery.header("x-nothing"), None);
}
#[test]
fn a_signed_header_that_arrived_twice_is_refused_rather_than_chosen_between() {
let headers = [
("Stripe-Signature", "t=1,v1=abc"),
("Accept", "*/*"),
("stripe-signature", "t=1,v1=forged"),
];
let delivery = Delivery::new(&headers, b"{}");
assert_eq!(delivery.header("Stripe-Signature"), Some("t=1,v1=abc"));
let repeated = delivery
.signed_header("Stripe-Signature")
.expect_err("two claims about one delivery are not one claim");
assert_eq!(repeated.count, 2);
assert!(repeated.to_string().contains("arrived 2 times"));
}
#[test]
fn a_header_that_never_arrived_is_not_the_same_failure_as_one_that_arrived_twice() {
let headers = [("Accept", "*/*")];
let delivery = Delivery::new(&headers, b"{}");
assert_eq!(
delivery
.signed_header("Stripe-Signature")
.expect("absent is not ambiguous"),
None
);
}
#[test]
fn a_body_that_is_not_utf8_is_kept_as_bytes_and_read_as_nothing() {
let delivery = Delivery::new(&[], &[0xff, 0xfe]);
assert_eq!(delivery.body(), &[0xff, 0xfe]);
assert!(delivery.body_str().is_none());
}
}