Skip to content

Commit 690c80a

Browse files
cursoragentlovasoa
andcommitted
Add webhook HMAC signature validation tests
Co-authored-by: contact <[email protected]>
1 parent 88a344a commit 690c80a

File tree

3 files changed

+166
-0
lines changed

3 files changed

+166
-0
lines changed

tests/requests/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,5 @@ async fn test_large_form_field_roundtrip() -> actix_web::Result<()> {
119119
);
120120
Ok(())
121121
}
122+
123+
mod webhook_hmac;

tests/requests/webhook_hmac.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
use actix_web::{http::StatusCode, test};
2+
use sqlpage::webserver::http::main_handler;
3+
4+
use crate::common::get_request_to;
5+
6+
#[actix_web::test]
7+
async fn test_webhook_hmac_invalid_signature() -> actix_web::Result<()> {
8+
// Set up environment variable for webhook secret
9+
std::env::set_var("WEBHOOK_SECRET", "test-secret-key");
10+
11+
let webhook_body = r#"{"order_id":12345,"total":"99.99"}"#;
12+
let invalid_signature = "invalid_signature_base64==";
13+
14+
let req = get_request_to("/tests/webhook_hmac_validation.sql")
15+
.await?
16+
.insert_header(("content-type", "application/json"))
17+
.insert_header(("X-Webhook-Signature", invalid_signature))
18+
.set_payload(webhook_body)
19+
.to_srv_request();
20+
21+
let resp = main_handler(req).await?;
22+
23+
// Should redirect to error page when signature is invalid
24+
assert!(
25+
resp.status() == StatusCode::FOUND || resp.status() == StatusCode::SEE_OTHER,
26+
"Expected redirect (302 or 303) for invalid signature, got: {}",
27+
resp.status()
28+
);
29+
30+
let location = resp
31+
.headers()
32+
.get("location")
33+
.expect("Should have Location header");
34+
let location_str = location.to_str().unwrap();
35+
assert!(
36+
location_str.contains("/error.sql"),
37+
"Should redirect to error page, got: {}",
38+
location_str
39+
);
40+
assert!(
41+
location_str.contains("Invalid+webhook+signature")
42+
|| location_str.contains("Invalid%20webhook%20signature"),
43+
"Error message should mention invalid signature, got: {}",
44+
location_str
45+
);
46+
47+
Ok(())
48+
}
49+
50+
#[actix_web::test]
51+
async fn test_webhook_hmac_valid_signature() -> actix_web::Result<()> {
52+
// Set up environment variable for webhook secret
53+
std::env::set_var("WEBHOOK_SECRET", "test-secret-key");
54+
55+
let webhook_body = r#"{"order_id":12345,"total":"99.99"}"#;
56+
57+
// Calculate the correct HMAC signature using the same algorithm
58+
use hmac::{Hmac, Mac};
59+
use sha2::Sha256;
60+
let mut mac = Hmac::<Sha256>::new_from_slice(b"test-secret-key").unwrap();
61+
mac.update(webhook_body.as_bytes());
62+
let result = mac.finalize();
63+
let valid_signature =
64+
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, result.into_bytes());
65+
66+
let req = get_request_to("/tests/webhook_hmac_validation.sql")
67+
.await?
68+
.insert_header(("content-type", "application/json"))
69+
.insert_header(("X-Webhook-Signature", valid_signature.as_str()))
70+
.set_payload(webhook_body)
71+
.to_srv_request();
72+
73+
let resp = main_handler(req).await?;
74+
75+
// Should return success when signature is valid
76+
assert_eq!(
77+
resp.status(),
78+
StatusCode::OK,
79+
"Expected OK status for valid signature"
80+
);
81+
82+
let body = test::read_body(resp).await;
83+
let body_str = String::from_utf8(body.to_vec()).unwrap();
84+
85+
// Should contain success message
86+
assert!(
87+
body_str.contains("success") || body_str.contains("Success"),
88+
"Response should indicate success, got: {}",
89+
body_str
90+
);
91+
assert!(
92+
body_str.contains("Webhook signature verified"),
93+
"Response should confirm signature verification, got: {}",
94+
body_str
95+
);
96+
assert!(
97+
body_str.contains("order_id"),
98+
"Response should contain the webhook body, got: {}",
99+
body_str
100+
);
101+
102+
Ok(())
103+
}
104+
105+
#[actix_web::test]
106+
async fn test_webhook_hmac_missing_signature() -> actix_web::Result<()> {
107+
// Set up environment variable for webhook secret
108+
std::env::set_var("WEBHOOK_SECRET", "test-secret-key");
109+
110+
let webhook_body = r#"{"order_id":12345,"total":"99.99"}"#;
111+
112+
// Don't include the X-Webhook-Signature header
113+
let req = get_request_to("/tests/webhook_hmac_validation.sql")
114+
.await?
115+
.insert_header(("content-type", "application/json"))
116+
.set_payload(webhook_body)
117+
.to_srv_request();
118+
119+
let resp = main_handler(req).await?;
120+
121+
// Should redirect to error page when signature is missing
122+
assert!(
123+
resp.status() == StatusCode::FOUND || resp.status() == StatusCode::SEE_OTHER,
124+
"Expected redirect (302 or 303) when signature header is missing, got: {}",
125+
resp.status()
126+
);
127+
128+
let location = resp
129+
.headers()
130+
.get("location")
131+
.expect("Should have Location header");
132+
let location_str = location.to_str().unwrap();
133+
assert!(
134+
location_str.contains("/error.sql"),
135+
"Should redirect to error page, got: {}",
136+
location_str
137+
);
138+
139+
Ok(())
140+
}

tests/webhook_hmac_validation.sql

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-- Webhook HMAC signature validation example
2+
-- This simulates receiving a webhook with HMAC signature in header
3+
4+
-- Redirect to error page if signature is missing
5+
SELECT 'redirect' as component,
6+
'/error.sql?message=' || sqlpage.url_encode('Missing webhook signature') as link
7+
WHERE sqlpage.header('X-Webhook-Signature') IS NULL;
8+
9+
-- Redirect to error page if signature is invalid
10+
SELECT 'redirect' as component,
11+
'/error.sql?message=' || sqlpage.url_encode('Invalid webhook signature') as link
12+
WHERE sqlpage.hmac(
13+
sqlpage.request_body(),
14+
sqlpage.environment_variable('WEBHOOK_SECRET'),
15+
'sha256-base64'
16+
) != sqlpage.header('X-Webhook-Signature');
17+
18+
-- If we reach here, signature is valid - return success
19+
SELECT 'json' as component;
20+
SELECT json_object(
21+
'status', 'success',
22+
'message', 'Webhook signature verified',
23+
'body', sqlpage.request_body()
24+
) as contents;

0 commit comments

Comments
 (0)