re-uploading work
This commit is contained in:
518
crates/api/tests/webhook_api_tests.rs
Normal file
518
crates/api/tests/webhook_api_tests.rs
Normal file
@@ -0,0 +1,518 @@
|
||||
//! 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);
|
||||
}
|
||||
Reference in New Issue
Block a user