use super::*;
use crate::auth::{GitHubAppId, PrivateKey, SecretProvider};
use crate::error::SecretError;
use async_trait::async_trait;
use chrono::Duration;
use std::sync::Arc;
struct MockSecretProvider {
webhook_secret: String,
}
impl MockSecretProvider {
fn new(secret: String) -> Self {
Self {
webhook_secret: secret,
}
}
}
#[async_trait]
impl SecretProvider for MockSecretProvider {
async fn get_private_key(&self) -> Result<PrivateKey, SecretError> {
Err(SecretError::NotFound {
key: "private_key".to_string(),
})
}
async fn get_app_id(&self) -> Result<GitHubAppId, SecretError> {
Ok(GitHubAppId::new(12345))
}
async fn get_webhook_secret(&self) -> Result<String, SecretError> {
Ok(self.webhook_secret.clone())
}
fn cache_duration(&self) -> Duration {
Duration::minutes(5)
}
}
#[tokio::test]
async fn test_validate_with_valid_signature() {
let secret = "test_webhook_secret";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let payload = br#"{"action":"opened","number":1,"pull_request":{"id":1}}"#;
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(payload);
let result = mac.finalize();
let expected_hex = hex::encode(result.into_bytes());
let signature = format!("sha256={}", expected_hex);
let is_valid = validator
.validate(payload, &signature)
.await
.expect("Validation should not error");
assert!(is_valid, "Valid signature should pass validation");
}
#[tokio::test]
async fn test_validate_with_github_example_payload() {
let secret = "It's a Secret to Everybody";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let payload = br#"{"zen":"Design for failure.","hook_id":1}"#;
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(payload);
let result = mac.finalize();
let expected_hex = hex::encode(result.into_bytes());
let signature = format!("sha256={}", expected_hex);
let is_valid = validator
.validate(payload, &signature)
.await
.expect("Validation should not error");
assert!(is_valid, "GitHub example signature should be valid");
}
#[tokio::test]
async fn test_validate_with_tampered_payload() {
let secret = "test_secret";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let original_payload = br#"{"action":"opened","number":1}"#;
let tampered_payload = br#"{"action":"closed","number":1}"#;
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(original_payload);
let result = mac.finalize();
let signature = format!("sha256={}", hex::encode(result.into_bytes()));
let is_valid = validator
.validate(tampered_payload, &signature)
.await
.expect("Validation should not error");
assert!(!is_valid, "Tampered payload should fail validation");
}
#[tokio::test]
async fn test_validate_with_wrong_secret() {
let correct_secret = "correct_secret";
let wrong_secret = "wrong_secret";
let payload = br#"{"action":"opened"}"#;
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(correct_secret.as_bytes()).unwrap();
mac.update(payload);
let result = mac.finalize();
let signature = format!("sha256={}", hex::encode(result.into_bytes()));
let secret_provider = Arc::new(MockSecretProvider::new(wrong_secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let is_valid = validator
.validate(payload, &signature)
.await
.expect("Validation should not error");
assert!(!is_valid, "Wrong secret should fail validation");
}
#[tokio::test]
async fn test_validate_with_modified_signature() {
let secret = "test_secret";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let payload = br#"{"action":"opened"}"#;
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(payload);
let result = mac.finalize();
let mut sig_bytes = hex::encode(result.into_bytes());
if let Some(ch) = sig_bytes.chars().next() {
let new_ch = if ch == 'a' { 'b' } else { 'a' };
sig_bytes = format!("{}{}", new_ch, &sig_bytes[1..]);
}
let signature = format!("sha256={}", sig_bytes);
let is_valid = validator
.validate(payload, &signature)
.await
.expect("Validation should not error");
assert!(!is_valid, "Modified signature should fail validation");
}
#[tokio::test]
async fn test_validate_with_missing_prefix() {
let secret = "test_secret";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let payload = br#"{"action":"opened"}"#;
let signature = "a1b2c3d4e5f6";
let result = validator.validate(payload, signature).await;
assert!(result.is_err(), "Missing prefix should return error");
if let Err(ValidationError::InvalidSignatureFormat { .. }) = result {
} else {
panic!("Expected InvalidSignatureFormat error");
}
}
#[tokio::test]
async fn test_validate_with_invalid_hex_encoding() {
let secret = "test_secret";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let payload = br#"{"action":"opened"}"#;
let signature = "sha256=not_valid_hex!!!";
let result = validator.validate(payload, signature).await;
assert!(result.is_err(), "Invalid hex should return error");
if let Err(ValidationError::InvalidSignatureFormat { .. }) = result {
} else {
panic!("Expected InvalidSignatureFormat error");
}
}
#[tokio::test]
async fn test_validate_with_empty_signature() {
let secret = "test_secret";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let payload = br#"{"action":"opened"}"#;
let signature = "";
let result = validator.validate(payload, signature).await;
assert!(result.is_err(), "Empty signature should return error");
}
#[tokio::test]
async fn test_validate_with_wrong_algorithm_prefix() {
let secret = "test_secret";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let payload = br#"{"action":"opened"}"#;
let signature = "sha1=a1b2c3d4e5f6";
let result = validator.validate(payload, signature).await;
assert!(
result.is_err(),
"Wrong algorithm prefix should return error"
);
}
#[tokio::test]
async fn test_validate_with_empty_payload() {
let secret = "test_secret";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let payload = b"";
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(payload);
let result = mac.finalize();
let signature = format!("sha256={}", hex::encode(result.into_bytes()));
let is_valid = validator
.validate(payload, &signature)
.await
.expect("Validation should not error");
assert!(is_valid, "Empty payload with valid signature should pass");
}
#[tokio::test]
async fn test_validate_with_large_payload() {
let secret = "test_secret";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let large_payload = vec![b'a'; 1024 * 1024];
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(&large_payload);
let result = mac.finalize();
let signature = format!("sha256={}", hex::encode(result.into_bytes()));
let start = std::time::Instant::now();
let is_valid = validator
.validate(&large_payload, &signature)
.await
.expect("Validation should not error");
let duration = start.elapsed();
assert!(is_valid, "Large payload should validate correctly");
assert!(
duration.as_millis() < 1000,
"Validation should complete in <1000ms, took {}ms",
duration.as_millis()
);
}
#[tokio::test]
async fn test_validate_with_unicode_in_payload() {
let secret = "test_secret";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let payload = r#"{"message":"Hello δΈη π"}"#.as_bytes();
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(payload);
let result = mac.finalize();
let signature = format!("sha256={}", hex::encode(result.into_bytes()));
let is_valid = validator
.validate(payload, &signature)
.await
.expect("Validation should not error");
assert!(is_valid, "Unicode payload should validate correctly");
}
#[tokio::test]
async fn test_validate_with_special_characters_in_secret() {
let secret = "my!@#$%^&*()secret_key";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let payload = br#"{"action":"opened"}"#;
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(payload);
let result = mac.finalize();
let signature = format!("sha256={}", hex::encode(result.into_bytes()));
let is_valid = validator
.validate(payload, &signature)
.await
.expect("Validation should not error");
assert!(
is_valid,
"Special characters in secret should work correctly"
);
}
#[tokio::test]
async fn test_uses_constant_time_comparison() {
let secret = "test_secret";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let payload = br#"{"action":"opened"}"#;
use hmac::{Hmac, KeyInit, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(payload);
let result = mac.finalize();
let result_bytes = result.into_bytes();
let valid_sig = format!("sha256={}", hex::encode(result_bytes));
let invalid_sig = "sha256=0000000000000000000000000000000000000000000000000000000000000000";
let valid_result = validator.validate(payload, &valid_sig).await;
let invalid_result = validator.validate(payload, invalid_sig).await;
assert!(valid_result.is_ok() && valid_result.unwrap());
assert!(invalid_result.is_ok() && !invalid_result.unwrap());
}
#[test]
fn test_debug_output_does_not_expose_secrets() {
let secret = "super_secret_webhook_key";
let secret_provider = Arc::new(MockSecretProvider::new(secret.to_string()));
let validator = SignatureValidator::new(secret_provider);
let debug_output = format!("{:?}", validator);
assert!(
!debug_output.contains(secret),
"Debug output should not contain secret"
);
assert!(
debug_output.contains("REDACTED"),
"Debug output should indicate redaction"
);
}