Merge pull request 'Webhooks: send one sample event to a saved webhook' (#100) from feature/webhook-test into main
ci / fork-checks (push) Canceled after 34s
ci / build (push) Canceled after 33s

This commit was merged in pull request #100.
This commit is contained in:
2026-09-29 01:06:13 +00:00
4 changed files with 221 additions and 9 deletions
+1
View File
@@ -19,6 +19,7 @@ pub mod ai_limits;
pub mod log_settings;
pub mod data_inventory;
pub mod directory_test;
pub mod webhook_test;
pub mod explanation;
pub mod protocol_policy;
pub mod tenant_protocol_policy;
+61
View File
@@ -0,0 +1,61 @@
/*
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
//! `POST /api/webhook/test`: send one sample event to a saved webhook
//! (settings-reorg, Webhooks "Send test").
//!
//! ```json
//! {"webhookId": "b"}
//! ```
//!
//! The answer is `{"sent": true, "status": 200, "ms": 84}` when the receiver
//! answered 2xx, `{"sent": false, "status": 403, …}` when it answered
//! otherwise, and `{"sent": false, "error": "…"}` when nothing came back. The
//! webhook is used as saved, even when it's off, so it can be tried before
//! it's switched on. The request goes where the saved webhook already sends,
//! so this gives nobody a reach they didn't have.
//!
//! For server-level administrators who may change webhooks.
use common::{Server, auth::AccessToken};
use registry::schema::{enums::Permission, structs::WebHook};
use serde_json::{Value, json};
use std::{str::FromStr, time::Instant};
use types::id::Id;
pub fn assert_allowed(access_token: &AccessToken) -> trc::Result<()> {
if access_token.tenant_id().is_some() {
return Err(trc::JmapEvent::Forbidden
.into_err()
.details("Webhook tests are for server-level administrators."));
}
access_token.enforce_permission(Permission::SysWebHookUpdate)
}
pub async fn test(server: &Server, body: &Value) -> trc::Result<Value> {
let webhook_id = body
.get("webhookId")
.and_then(Value::as_str)
.and_then(|id| Id::from_str(id).ok())
.ok_or_else(|| {
trc::ResourceEvent::BadParameters
.into_err()
.details("Expected {\"webhookId\": …}")
})?;
let Some(hook) = server.registry().object::<WebHook>(webhook_id).await? else {
return Ok(json!({ "sent": false, "error": "There's no such webhook. Save it first." }));
};
let started = Instant::now();
Ok(match common::telemetry::webhooks::send_test(&hook).await {
Ok(status) => json!({
"sent": (200..300).contains(&status),
"status": status,
"ms": started.elapsed().as_millis() as u64,
}),
Err(error) => json!({ "sent": false, "error": error }),
})
}