Skip to main content

uselesskey_webhook/
fixture.rs

1use rand_chacha10::ChaCha20Rng;
2use rand_core10::{Rng, SeedableRng};
3use uselesskey_core::Factory;
4
5use crate::payload::{canonical_payload, stable_spec_bytes};
6use crate::secret::build_secret;
7use crate::signature::sign;
8use crate::{
9    DOMAIN_WEBHOOK_FIXTURE, NearMissScenario, NearMissWebhookFixture, WebhookFixture,
10    WebhookPayloadSpec, WebhookProfile,
11};
12
13/// Extension trait to generate webhook fixtures from [`Factory`].
14pub trait WebhookFactoryExt {
15    /// Generate a webhook fixture for an explicit profile.
16    fn webhook(
17        &self,
18        profile: WebhookProfile,
19        label: impl AsRef<str>,
20        payload_spec: WebhookPayloadSpec,
21    ) -> WebhookFixture;
22
23    /// Generate a GitHub webhook fixture.
24    fn webhook_github(
25        &self,
26        label: impl AsRef<str>,
27        payload_spec: WebhookPayloadSpec,
28    ) -> WebhookFixture;
29
30    /// Generate a Stripe webhook fixture.
31    fn webhook_stripe(
32        &self,
33        label: impl AsRef<str>,
34        payload_spec: WebhookPayloadSpec,
35    ) -> WebhookFixture;
36
37    /// Generate a Slack webhook fixture.
38    fn webhook_slack(
39        &self,
40        label: impl AsRef<str>,
41        payload_spec: WebhookPayloadSpec,
42    ) -> WebhookFixture;
43}
44
45impl WebhookFactoryExt for Factory {
46    fn webhook(
47        &self,
48        profile: WebhookProfile,
49        label: impl AsRef<str>,
50        payload_spec: WebhookPayloadSpec,
51    ) -> WebhookFixture {
52        let label = label.as_ref();
53        let spec_bytes = stable_spec_bytes(profile, &payload_spec);
54        let cached = self.get_or_init(DOMAIN_WEBHOOK_FIXTURE, label, &spec_bytes, "good", |seed| {
55            build_fixture_from_seed(profile, label, payload_spec.clone(), seed.bytes())
56        });
57        cached.as_ref().clone()
58    }
59
60    fn webhook_github(
61        &self,
62        label: impl AsRef<str>,
63        payload_spec: WebhookPayloadSpec,
64    ) -> WebhookFixture {
65        self.webhook(WebhookProfile::GitHub, label, payload_spec)
66    }
67
68    fn webhook_stripe(
69        &self,
70        label: impl AsRef<str>,
71        payload_spec: WebhookPayloadSpec,
72    ) -> WebhookFixture {
73        self.webhook(WebhookProfile::Stripe, label, payload_spec)
74    }
75
76    fn webhook_slack(
77        &self,
78        label: impl AsRef<str>,
79        payload_spec: WebhookPayloadSpec,
80    ) -> WebhookFixture {
81        self.webhook(WebhookProfile::Slack, label, payload_spec)
82    }
83}
84
85impl WebhookFixture {
86    /// Produce a stale-timestamp variant for replay-window tests.
87    pub fn near_miss_stale_timestamp(&self, max_age_secs: i64) -> NearMissWebhookFixture {
88        let stale_ts = self.timestamp - max_age_secs - 1;
89        let mut f = self.with_timestamp(stale_ts);
90        f.scenario = NearMissScenario::StaleTimestamp;
91        f
92    }
93
94    /// Produce a wrong-secret variant for verifier mismatch tests.
95    pub fn near_miss_wrong_secret(&self) -> NearMissWebhookFixture {
96        let mut wrong_secret = self.secret.clone();
97        wrong_secret.push_str("_wrong");
98        let mut f = build_near_miss(
99            self.profile,
100            wrong_secret,
101            self.payload.clone(),
102            self.timestamp,
103        );
104        f.scenario = NearMissScenario::WrongSecret;
105        f
106    }
107
108    /// Produce a tampered-payload variant for integrity tests.
109    pub fn near_miss_tampered_payload(&self) -> NearMissWebhookFixture {
110        let tampered = format!("{}{}", self.payload, "\n");
111        let mut f = build_near_miss(self.profile, self.secret.clone(), tampered, self.timestamp);
112        f.scenario = NearMissScenario::TamperedPayload;
113        f
114    }
115
116    fn with_timestamp(&self, timestamp: i64) -> NearMissWebhookFixture {
117        build_near_miss(
118            self.profile,
119            self.secret.clone(),
120            self.payload.clone(),
121            timestamp,
122        )
123    }
124}
125
126fn build_near_miss(
127    profile: WebhookProfile,
128    secret: String,
129    payload: String,
130    timestamp: i64,
131) -> NearMissWebhookFixture {
132    let (headers, signature_input) = sign(profile, &secret, &payload, timestamp);
133    NearMissWebhookFixture {
134        scenario: NearMissScenario::StaleTimestamp,
135        profile,
136        secret,
137        payload,
138        headers,
139        timestamp,
140        signature_input,
141    }
142}
143
144pub(crate) fn build_fixture_from_seed(
145    profile: WebhookProfile,
146    label: &str,
147    payload_spec: WebhookPayloadSpec,
148    seed: &[u8; 32],
149) -> WebhookFixture {
150    let mut rng = ChaCha20Rng::from_seed(*seed);
151    let secret = build_secret(profile, &mut rng);
152    let timestamp = 1_700_000_000_i64 + (rng.next_u32() as i64 % 200_000_000_i64);
153    let payload = canonical_payload(profile, label, payload_spec, rng.next_u32());
154    let (headers, signature_input) = sign(profile, &secret, &payload, timestamp);
155
156    WebhookFixture {
157        profile,
158        secret,
159        payload,
160        headers,
161        timestamp,
162        signature_input,
163    }
164}