#![cfg(feature = "integration-tests")] //! Comprehensive integration tests for webhook security features (Phase 3) //! //! Tests cover: //! - HMAC signature verification (SHA256, SHA512, SHA1) //! - Rate limiting //! - IP whitelisting //! - Payload size limits //! - Event logging //! - Error scenarios use attune_api::{AppState, Server}; use attune_common::{ config::Config, db::Database, repositories::{ pack::{CreatePackInput, PackRepository}, trigger::{CreateTriggerInput, TriggerRepository}, Create, }, }; use axum::{ body::Body, http::{Request, StatusCode}, }; use serde_json::json; use tower::ServiceExt; /// Helper to create test database and state async fn setup_test_state() -> AppState { let config = Config::load().expect("Failed to load config"); let database = Database::new(&config.database) .await .expect("Failed to connect to database"); AppState::new(database.pool().clone(), config) } /// Helper to create a test pack async fn create_test_pack(state: &AppState, name: &str) -> i64 { let input = CreatePackInput { r#ref: name.to_string(), label: format!("{} Pack", name), description: Some(format!("Test pack for {}", name)), version: "1.0.0".to_string(), conf_schema: serde_json::json!({}), config: serde_json::json!({}), meta: serde_json::json!({}), tags: vec![], runtime_deps: vec![], dependencies: vec![], is_standard: false, installers: json!({}), }; let pack = PackRepository::create(&state.db, input) .await .expect("Failed to create pack"); pack.id } /// Helper to create a test trigger async fn create_test_trigger( state: &AppState, pack_id: i64, pack_ref: &str, trigger_ref: &str, ) -> i64 { let input = CreateTriggerInput { r#ref: trigger_ref.to_string(), pack: Some(pack_id), pack_ref: Some(pack_ref.to_string()), label: format!("{} Trigger", trigger_ref), description: Some(format!("Test trigger {}", trigger_ref)), enabled: true, param_schema: None, out_schema: None, is_adhoc: false, }; let trigger = TriggerRepository::create(&state.db, input) .await .expect("Failed to create trigger"); trigger.id } /// Helper to generate HMAC signature fn generate_hmac_signature(payload: &[u8], secret: &str, algorithm: &str) -> String { use hmac::{Hmac, Mac}; use sha1::Sha1; use sha2::{Sha256, Sha512}; match algorithm { "sha256" => { type HmacSha256 = Hmac; let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap(); mac.update(payload); let result = mac.finalize(); format!("sha256={}", hex::encode(result.into_bytes())) } "sha512" => { type HmacSha512 = Hmac; let mut mac = HmacSha512::new_from_slice(secret.as_bytes()).unwrap(); mac.update(payload); let result = mac.finalize(); format!("sha512={}", hex::encode(result.into_bytes())) } "sha1" => { type HmacSha1 = Hmac; let mut mac = HmacSha1::new_from_slice(secret.as_bytes()).unwrap(); mac.update(payload); let result = mac.finalize(); format!("sha1={}", hex::encode(result.into_bytes())) } _ => panic!("Unsupported algorithm: {}", algorithm), } } // ============================================================================ // HMAC SIGNATURE TESTS // ============================================================================ #[tokio::test] async fn test_webhook_hmac_sha256_valid() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); // Create test data let pack_id = create_test_pack(&state, "hmac_sha256_test").await; let trigger_id = create_test_trigger( &state, pack_id, "hmac_sha256_test", "hmac_sha256_test.trigger", ) .await; // Enable webhooks let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); // Configure HMAC let hmac_secret = "test-secret-key-12345"; sqlx::query( "UPDATE attune.trigger SET webhook_hmac_enabled = true, webhook_hmac_algorithm = 'sha256', webhook_hmac_secret = $1 WHERE id = $2", ) .bind(hmac_secret) .bind(trigger_id) .execute(&state.db) .await .expect("Failed to configure HMAC"); // Prepare webhook payload let webhook_payload = json!({ "payload": { "event": "test_event", "data": {"foo": "bar"} } }); let payload_bytes = serde_json::to_vec(&webhook_payload).unwrap(); // Generate valid signature let signature = generate_hmac_signature(&payload_bytes, hmac_secret, "sha256"); // Send webhook with valid signature let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .header("x-webhook-signature", signature) .body(Body::from(payload_bytes)) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_webhook_hmac_sha512_valid() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "hmac_sha512_test").await; let trigger_id = create_test_trigger( &state, pack_id, "hmac_sha512_test", "hmac_sha512_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); let hmac_secret = "test-secret-sha512"; sqlx::query( "UPDATE attune.trigger SET webhook_hmac_enabled = true, webhook_hmac_algorithm = 'sha512', webhook_hmac_secret = $1 WHERE id = $2", ) .bind(hmac_secret) .bind(trigger_id) .execute(&state.db) .await .expect("Failed to configure HMAC"); let webhook_payload = json!({ "payload": {"message": "test"} }); let payload_bytes = serde_json::to_vec(&webhook_payload).unwrap(); let signature = generate_hmac_signature(&payload_bytes, hmac_secret, "sha512"); let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .header("x-webhook-signature", signature) .body(Body::from(payload_bytes)) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_webhook_hmac_invalid_signature() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "hmac_invalid_test").await; let trigger_id = create_test_trigger( &state, pack_id, "hmac_invalid_test", "hmac_invalid_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); let hmac_secret = "test-secret-key"; sqlx::query( "UPDATE attune.trigger SET webhook_hmac_enabled = true, webhook_hmac_algorithm = 'sha256', webhook_hmac_secret = $1 WHERE id = $2", ) .bind(hmac_secret) .bind(trigger_id) .execute(&state.db) .await .expect("Failed to configure HMAC"); let webhook_payload = json!({ "payload": {"message": "test"} }); // Send with invalid signature let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .header("x-webhook-signature", "sha256=invalid_signature_here") .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn test_webhook_hmac_missing_signature() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "hmac_missing_test").await; let trigger_id = create_test_trigger( &state, pack_id, "hmac_missing_test", "hmac_missing_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); sqlx::query( "UPDATE attune.trigger SET webhook_hmac_enabled = true, webhook_hmac_algorithm = 'sha256', webhook_hmac_secret = 'secret' WHERE id = $1", ) .bind(trigger_id) .execute(&state.db) .await .expect("Failed to configure HMAC"); let webhook_payload = json!({ "payload": {"message": "test"} }); // Send without signature header let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } #[tokio::test] async fn test_webhook_hmac_wrong_secret() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "hmac_wrong_secret_test").await; let trigger_id = create_test_trigger( &state, pack_id, "hmac_wrong_secret_test", "hmac_wrong_secret_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); let hmac_secret = "correct-secret"; sqlx::query( "UPDATE attune.trigger SET webhook_hmac_enabled = true, webhook_hmac_algorithm = 'sha256', webhook_hmac_secret = $1 WHERE id = $2", ) .bind(hmac_secret) .bind(trigger_id) .execute(&state.db) .await .expect("Failed to configure HMAC"); let webhook_payload = json!({ "payload": {"message": "test"} }); let payload_bytes = serde_json::to_vec(&webhook_payload).unwrap(); // Generate signature with wrong secret let wrong_signature = generate_hmac_signature(&payload_bytes, "wrong-secret", "sha256"); let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .header("x-webhook-signature", wrong_signature) .body(Body::from(payload_bytes)) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); } // ============================================================================ // RATE LIMITING TESTS // ============================================================================ #[tokio::test] async fn test_webhook_rate_limit_enforced() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let pack_id = create_test_pack(&state, "rate_limit_test").await; let trigger_id = create_test_trigger( &state, pack_id, "rate_limit_test", "rate_limit_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); // Configure rate limit: 3 requests per 60 seconds sqlx::query( "UPDATE attune.trigger SET webhook_rate_limit_enabled = true, webhook_rate_limit_requests = 3, webhook_rate_limit_window_seconds = 60 WHERE id = $1", ) .bind(trigger_id) .execute(&state.db) .await .expect("Failed to configure rate limit"); let webhook_payload = json!({ "payload": {"message": "test"} }); // Send 3 requests (should succeed) for i in 0..3 { let app = server.router(); let response = app .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!( response.status(), StatusCode::OK, "Request {} should succeed", i + 1 ); } // 4th request should be rate limited let app = server.router(); let response = app .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS); } #[tokio::test] async fn test_webhook_rate_limit_disabled() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let pack_id = create_test_pack(&state, "no_rate_limit_test").await; let trigger_id = create_test_trigger( &state, pack_id, "no_rate_limit_test", "no_rate_limit_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); // Ensure rate limiting is disabled (default) let webhook_payload = json!({ "payload": {"message": "test"} }); // Send multiple requests - all should succeed for _ in 0..10 { let app = server.router(); let response = app .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } } // ============================================================================ // IP WHITELISTING TESTS // ============================================================================ #[tokio::test] async fn test_webhook_ip_whitelist_allowed() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "ip_whitelist_test").await; let trigger_id = create_test_trigger( &state, pack_id, "ip_whitelist_test", "ip_whitelist_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); // Configure IP whitelist sqlx::query( "UPDATE attune.trigger SET webhook_ip_whitelist_enabled = true, webhook_ip_whitelist = ARRAY['192.168.1.0/24', '10.0.0.1'] WHERE id = $1", ) .bind(trigger_id) .execute(&state.db) .await .expect("Failed to configure IP whitelist"); let webhook_payload = json!({ "payload": {"message": "test"} }); // Test with allowed IP in CIDR range let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .header("x-forwarded-for", "192.168.1.100") .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); // Test with exact match IP let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .header("x-forwarded-for", "10.0.0.1") .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } #[tokio::test] async fn test_webhook_ip_whitelist_blocked() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "ip_blocked_test").await; let trigger_id = create_test_trigger( &state, pack_id, "ip_blocked_test", "ip_blocked_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); sqlx::query( "UPDATE attune.trigger SET webhook_ip_whitelist_enabled = true, webhook_ip_whitelist = ARRAY['192.168.1.0/24'] WHERE id = $1", ) .bind(trigger_id) .execute(&state.db) .await .expect("Failed to configure IP whitelist"); let webhook_payload = json!({ "payload": {"message": "test"} }); // Test with IP not in whitelist let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .header("x-forwarded-for", "8.8.8.8") .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::FORBIDDEN); } // ============================================================================ // PAYLOAD SIZE LIMIT TESTS // ============================================================================ #[tokio::test] async fn test_webhook_payload_size_limit_enforced() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "size_limit_test").await; let trigger_id = create_test_trigger( &state, pack_id, "size_limit_test", "size_limit_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); // Set small payload limit: 1 KB sqlx::query("UPDATE attune.trigger SET webhook_payload_size_limit_kb = 1 WHERE id = $1") .bind(trigger_id) .execute(&state.db) .await .expect("Failed to set payload size limit"); // Create a large payload (> 1 KB) let large_data = "x".repeat(2000); let webhook_payload = json!({ "payload": { "large_field": large_data } }); let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_webhook_payload_size_within_limit() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "size_ok_test").await; let trigger_id = create_test_trigger(&state, pack_id, "size_ok_test", "size_ok_test.trigger").await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); // Set payload limit: 10 KB sqlx::query("UPDATE attune.trigger SET webhook_payload_size_limit_kb = 10 WHERE id = $1") .bind(trigger_id) .execute(&state.db) .await .expect("Failed to set payload size limit"); // Create a small payload (< 10 KB) let webhook_payload = json!({ "payload": { "message": "This is a small payload" } }); let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); } // ============================================================================ // EVENT LOGGING TESTS // ============================================================================ #[tokio::test] #[ignore] async fn test_webhook_event_logging_success() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "event_log_test").await; let trigger_id = create_test_trigger(&state, pack_id, "event_log_test", "event_log_test.trigger").await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); let webhook_payload = json!({ "payload": {"message": "test"} }); let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .header("x-forwarded-for", "192.168.1.1") .header("user-agent", "TestAgent/1.0") .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); // Verify event was logged let log_count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM attune.webhook_event_log WHERE trigger_id = $1") .bind(trigger_id) .fetch_one(&state.db) .await .expect("Failed to check event log"); assert!(log_count.0 > 0, "Event should be logged"); // Check log details let log: (i32, Option, Option) = sqlx::query_as( "SELECT status_code, source_ip, user_agent FROM attune.webhook_event_log WHERE trigger_id = $1 ORDER BY created DESC LIMIT 1", ) .bind(trigger_id) .fetch_one(&state.db) .await .expect("Failed to fetch log details"); assert_eq!(log.0, 200); assert_eq!(log.1.as_deref(), Some("192.168.1.1")); assert_eq!(log.2.as_deref(), Some("TestAgent/1.0")); } #[tokio::test] #[ignore] async fn test_webhook_event_logging_failure() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "event_log_fail_test").await; let trigger_id = create_test_trigger( &state, pack_id, "event_log_fail_test", "event_log_fail_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); // Configure HMAC to force failure sqlx::query( "UPDATE attune.trigger SET webhook_hmac_enabled = true, webhook_hmac_algorithm = 'sha256', webhook_hmac_secret = 'secret' WHERE id = $1", ) .bind(trigger_id) .execute(&state.db) .await .expect("Failed to configure HMAC"); let webhook_payload = json!({ "payload": {"message": "test"} }); // Send without signature (should fail) let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::UNAUTHORIZED); // Verify failure was logged let log: (i32, Option, Option) = sqlx::query_as( "SELECT status_code, error_message, hmac_verified FROM attune.webhook_event_log WHERE trigger_id = $1 ORDER BY created DESC LIMIT 1", ) .bind(trigger_id) .fetch_one(&state.db) .await .expect("Failed to fetch log details"); assert_eq!(log.0, 401); assert!(log.1.is_some()); assert_eq!(log.2, Some(false)); } // ============================================================================ // COMBINED SECURITY FEATURES TESTS // ============================================================================ #[tokio::test] #[ignore] async fn test_webhook_all_security_features_pass() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "all_features_test").await; let trigger_id = create_test_trigger( &state, pack_id, "all_features_test", "all_features_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); let hmac_secret = "all-features-secret"; // Enable all security features sqlx::query( "UPDATE attune.trigger SET webhook_hmac_enabled = true, webhook_hmac_algorithm = 'sha256', webhook_hmac_secret = $1, webhook_rate_limit_enabled = true, webhook_rate_limit_requests = 10, webhook_rate_limit_window_seconds = 60, webhook_ip_whitelist_enabled = true, webhook_ip_whitelist = ARRAY['192.168.1.0/24'], webhook_payload_size_limit_kb = 10 WHERE id = $2", ) .bind(hmac_secret) .bind(trigger_id) .execute(&state.db) .await .expect("Failed to configure all features"); let webhook_payload = json!({ "payload": {"message": "test with all features"} }); let payload_bytes = serde_json::to_vec(&webhook_payload).unwrap(); let signature = generate_hmac_signature(&payload_bytes, hmac_secret, "sha256"); // Send webhook that passes all checks let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .header("x-webhook-signature", signature) .header("x-forwarded-for", "192.168.1.50") .header("user-agent", "TestClient/1.0") .body(Body::from(payload_bytes)) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::OK); // Verify event log shows all checks passed let log: (Option, bool, Option) = sqlx::query_as( "SELECT hmac_verified, rate_limited, ip_allowed FROM attune.webhook_event_log WHERE trigger_id = $1 ORDER BY created DESC LIMIT 1", ) .bind(trigger_id) .fetch_one(&state.db) .await .expect("Failed to fetch log details"); assert_eq!(log.0, Some(true)); // HMAC verified assert!(!log.1); // Not rate limited assert_eq!(log.2, Some(true)); // IP allowed } #[tokio::test] #[ignore] async fn test_webhook_multiple_security_failures() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "multi_fail_test").await; let trigger_id = create_test_trigger( &state, pack_id, "multi_fail_test", "multi_fail_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); // Enable multiple security features sqlx::query( "UPDATE attune.trigger SET webhook_hmac_enabled = true, webhook_hmac_algorithm = 'sha256', webhook_hmac_secret = 'secret', webhook_ip_whitelist_enabled = true, webhook_ip_whitelist = ARRAY['10.0.0.0/8'] WHERE id = $1", ) .bind(trigger_id) .execute(&state.db) .await .expect("Failed to configure features"); let webhook_payload = json!({ "payload": {"message": "test"} }); // Send webhook that fails multiple checks (wrong IP, missing signature) let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .header("x-forwarded-for", "8.8.8.8") // Wrong IP // Missing signature header .body(Body::from(serde_json::to_string(&webhook_payload).unwrap())) .unwrap(), ) .await .unwrap(); // Should fail on IP check first assert_eq!(response.status(), StatusCode::FORBIDDEN); } // ============================================================================ // EDGE CASES AND ERROR SCENARIOS // ============================================================================ #[tokio::test] #[ignore] async fn test_webhook_malformed_json() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "malformed_json_test").await; let trigger_id = create_test_trigger( &state, pack_id, "malformed_json_test", "malformed_json_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); // Send malformed JSON let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .body(Body::from("{invalid json here")) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); } #[tokio::test] #[ignore] async fn test_webhook_empty_payload() { let state = setup_test_state().await; let server = Server::new(std::sync::Arc::new(state.clone())); let app = server.router(); let pack_id = create_test_pack(&state, "empty_payload_test").await; let trigger_id = create_test_trigger( &state, pack_id, "empty_payload_test", "empty_payload_test.trigger", ) .await; let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id) .await .expect("Failed to enable webhook"); // Send empty body let response = app .clone() .oneshot( Request::builder() .method("POST") .uri(format!("/api/v1/webhooks/{}", webhook_info.webhook_key)) .header("content-type", "application/json") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::BAD_REQUEST); }