Files
attune/crates/api/tests/webhook_api_tests.rs
2026-02-04 17:46:30 -06:00

519 lines
15 KiB
Rust

//! Integration tests for webhook API endpoints
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![],
is_standard: false,
};
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 get JWT token for authenticated requests
async fn get_auth_token(app: &axum::Router, username: &str, password: &str) -> String {
let login_request = json!({
"username": username,
"password": password
});
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/auth/login")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&login_request).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
json["data"]["access_token"].as_str().unwrap().to_string()
}
#[tokio::test]
#[ignore] // Run with --ignored flag when database is available
async fn test_enable_webhook() {
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, "webhook_test").await;
let _trigger_id =
create_test_trigger(&state, pack_id, "webhook_test", "webhook_test.trigger").await;
// Get auth token (assumes a test user exists)
let token = get_auth_token(&app, "test_user", "test_password").await;
// Enable webhooks
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/triggers/webhook_test.trigger/webhooks/enable")
.header("authorization", format!("Bearer {}", token))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
// Verify response structure
assert!(json["data"]["webhook_enabled"].as_bool().unwrap());
assert!(json["data"]["webhook_key"].is_string());
let webhook_key = json["data"]["webhook_key"].as_str().unwrap();
assert!(webhook_key.starts_with("wh_"));
}
#[tokio::test]
#[ignore]
async fn test_disable_webhook() {
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, "webhook_disable_test").await;
let trigger_id = create_test_trigger(
&state,
pack_id,
"webhook_disable_test",
"webhook_disable_test.trigger",
)
.await;
// Enable webhooks first
let _ = TriggerRepository::enable_webhook(&state.db, trigger_id)
.await
.expect("Failed to enable webhook");
// Get auth token
let token = get_auth_token(&app, "test_user", "test_password").await;
// Disable webhooks
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/triggers/webhook_disable_test.trigger/webhooks/disable")
.header("authorization", format!("Bearer {}", token))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
// Verify webhooks are disabled
assert!(!json["data"]["webhook_enabled"].as_bool().unwrap());
assert!(json["data"]["webhook_key"].is_null());
}
#[tokio::test]
#[ignore]
async fn test_regenerate_webhook_key() {
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, "webhook_regen_test").await;
let trigger_id = create_test_trigger(
&state,
pack_id,
"webhook_regen_test",
"webhook_regen_test.trigger",
)
.await;
// Enable webhooks first
let original_info = TriggerRepository::enable_webhook(&state.db, trigger_id)
.await
.expect("Failed to enable webhook");
// Get auth token
let token = get_auth_token(&app, "test_user", "test_password").await;
// Regenerate webhook key
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/triggers/webhook_regen_test.trigger/webhooks/regenerate")
.header("authorization", format!("Bearer {}", token))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
// Verify new key is different from original
let new_key = json["data"]["webhook_key"].as_str().unwrap();
assert_ne!(new_key, original_info.webhook_key);
assert!(new_key.starts_with("wh_"));
}
#[tokio::test]
#[ignore]
async fn test_regenerate_webhook_key_not_enabled() {
let state = setup_test_state().await;
let server = Server::new(std::sync::Arc::new(state.clone()));
let app = server.router();
// Create test data without enabling webhooks
let pack_id = create_test_pack(&state, "webhook_not_enabled_test").await;
let _trigger_id = create_test_trigger(
&state,
pack_id,
"webhook_not_enabled_test",
"webhook_not_enabled_test.trigger",
)
.await;
// Get auth token
let token = get_auth_token(&app, "test_user", "test_password").await;
// Try to regenerate without enabling first
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/triggers/webhook_not_enabled_test.trigger/webhooks/regenerate")
.header("authorization", format!("Bearer {}", token))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test]
#[ignore]
async fn test_receive_webhook() {
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, "webhook_receive_test").await;
let trigger_id = create_test_trigger(
&state,
pack_id,
"webhook_receive_test",
"webhook_receive_test.trigger",
)
.await;
// Enable webhooks
let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id)
.await
.expect("Failed to enable webhook");
// Send webhook
let webhook_payload = json!({
"payload": {
"event": "test_event",
"data": {
"foo": "bar",
"number": 42
}
},
"headers": {
"X-Test-Header": "test-value"
},
"source_ip": "192.168.1.1",
"user_agent": "Test Agent/1.0"
});
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);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
// Verify response
assert!(json["data"]["event_id"].is_number());
assert_eq!(
json["data"]["trigger_ref"].as_str().unwrap(),
"webhook_receive_test.trigger"
);
assert!(json["data"]["received_at"].is_string());
assert_eq!(
json["data"]["message"].as_str().unwrap(),
"Webhook received successfully"
);
}
#[tokio::test]
#[ignore]
async fn test_receive_webhook_invalid_key() {
let state = setup_test_state().await;
let server = Server::new(std::sync::Arc::new(state));
let app = server.router();
// Try to send webhook with invalid key
let webhook_payload = json!({
"payload": {
"event": "test_event"
}
});
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/webhooks/wh_invalid_key_12345")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&webhook_payload).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
#[ignore]
async fn test_receive_webhook_disabled() {
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, "webhook_disabled_test").await;
let trigger_id = create_test_trigger(
&state,
pack_id,
"webhook_disabled_test",
"webhook_disabled_test.trigger",
)
.await;
// Enable then disable webhooks
let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id)
.await
.expect("Failed to enable webhook");
TriggerRepository::disable_webhook(&state.db, trigger_id)
.await
.expect("Failed to disable webhook");
// Try to send webhook with disabled key
let webhook_payload = json!({
"payload": {
"event": "test_event"
}
});
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();
// Should return 404 because disabled webhook keys are not found
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test]
#[ignore]
async fn test_webhook_requires_auth_for_management() {
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, "webhook_auth_test").await;
let _trigger_id = create_test_trigger(
&state,
pack_id,
"webhook_auth_test",
"webhook_auth_test.trigger",
)
.await;
// Try to enable without auth
let response = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/triggers/webhook_auth_test.trigger/webhooks/enable")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
#[ignore]
async fn test_receive_webhook_minimal_payload() {
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, "webhook_minimal_test").await;
let trigger_id = create_test_trigger(
&state,
pack_id,
"webhook_minimal_test",
"webhook_minimal_test.trigger",
)
.await;
// Enable webhooks
let webhook_info = TriggerRepository::enable_webhook(&state.db, trigger_id)
.await
.expect("Failed to enable webhook");
// Send webhook with minimal payload (only required fields)
let webhook_payload = json!({
"payload": {
"message": "minimal test"
}
});
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);
}