Skip to main content

uselesskey_webhook/
model.rs

1use std::collections::BTreeMap;
2use std::fmt;
3
4/// Supported webhook signing profiles.
5#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
6pub enum WebhookProfile {
7    /// GitHub webhook signature profile.
8    GitHub,
9    /// Stripe webhook signature profile.
10    Stripe,
11    /// Slack webhook signature profile.
12    Slack,
13}
14
15impl WebhookProfile {
16    pub(crate) fn stable_tag(self) -> &'static str {
17        match self {
18            Self::GitHub => "github",
19            Self::Stripe => "stripe",
20            Self::Slack => "slack",
21        }
22    }
23}
24
25/// Canonical payload presets for webhook fixtures.
26#[derive(Clone, Debug, Eq, PartialEq)]
27pub enum WebhookPayloadSpec {
28    /// Use the built-in provider canonical payload template.
29    Canonical,
30    /// Use an explicit payload string.
31    Raw(String),
32}
33
34impl WebhookPayloadSpec {
35    pub(crate) fn stable_bytes(&self) -> Vec<u8> {
36        match self {
37            Self::Canonical => b"canonical".to_vec(),
38            Self::Raw(payload) => {
39                let mut out = b"raw:".to_vec();
40                out.extend_from_slice(payload.as_bytes());
41                out
42            }
43        }
44    }
45}
46
47/// A generated webhook fixture.
48#[derive(Clone)]
49pub struct WebhookFixture {
50    /// Profile used to generate fixture semantics.
51    pub profile: WebhookProfile,
52    /// Signing secret (test-only).
53    pub secret: String,
54    /// Canonical payload body.
55    pub payload: String,
56    /// HTTP headers to attach to the request.
57    pub headers: BTreeMap<String, String>,
58    /// Timestamp used in signature generation (unix epoch seconds).
59    pub timestamp: i64,
60    /// Canonical signature input/base string.
61    pub signature_input: String,
62}
63
64impl fmt::Debug for WebhookFixture {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.debug_struct("WebhookFixture")
67            .field("profile", &self.profile)
68            .field("payload", &self.payload)
69            .field("headers", &self.headers)
70            .field("timestamp", &self.timestamp)
71            .field("signature_input", &self.signature_input)
72            .finish_non_exhaustive()
73    }
74}
75
76/// A near-miss webhook fixture for negative tests.
77#[derive(Clone)]
78pub struct NearMissWebhookFixture {
79    /// Negative scenario marker.
80    pub scenario: NearMissScenario,
81    /// Profile used to generate fixture semantics.
82    pub profile: WebhookProfile,
83    /// Signing secret (intentionally wrong for `WrongSecret`).
84    pub secret: String,
85    /// Payload body (intentionally modified for `TamperedPayload`).
86    pub payload: String,
87    /// HTTP headers to attach to the request.
88    pub headers: BTreeMap<String, String>,
89    /// Timestamp used in signature generation (may be stale).
90    pub timestamp: i64,
91    /// Canonical signature input/base string.
92    pub signature_input: String,
93}
94
95impl fmt::Debug for NearMissWebhookFixture {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        f.debug_struct("NearMissWebhookFixture")
98            .field("scenario", &self.scenario)
99            .field("profile", &self.profile)
100            .field("payload", &self.payload)
101            .field("headers", &self.headers)
102            .field("timestamp", &self.timestamp)
103            .field("signature_input", &self.signature_input)
104            .finish_non_exhaustive()
105    }
106}
107
108/// Supported near-miss negative scenarios.
109#[derive(Clone, Copy, Debug, Eq, PartialEq)]
110pub enum NearMissScenario {
111    /// Header timestamp falls outside the acceptable window.
112    StaleTimestamp,
113    /// Request signed with an alternate secret not used by verifier.
114    WrongSecret,
115    /// Payload differs from what was signed.
116    TamperedPayload,
117}