f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
304 lines
9.5 KiB
Rust
304 lines
9.5 KiB
Rust
//! E2E tests for the Webhook management + tag-settings + tag-bindings endpoints.
|
|
|
|
mod common;
|
|
|
|
use axum::http::StatusCode;
|
|
use serde_json::json;
|
|
use tower::ServiceExt;
|
|
|
|
use common::{body_json, build_app, delete_with_token, get_request, get_with_token, json_with_token, setup_and_login};
|
|
|
|
#[tokio::test]
|
|
async fn unauthenticated_webhook_list_is_rejected() {
|
|
let (app, _services) = build_app().await;
|
|
let resp = app.oneshot(get_request("/api/webhooks")).await.unwrap();
|
|
assert!(
|
|
resp.status() == StatusCode::UNAUTHORIZED || resp.status() == StatusCode::FORBIDDEN,
|
|
"expected 401/403, got {}",
|
|
resp.status()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn webhook_crud_and_secret_is_hidden() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
// create (with a secret)
|
|
let body = json!({
|
|
"name": "Team bot",
|
|
"url": "https://open.feishu.cn/open-apis/bot/v2/hook/abc",
|
|
"platform": "lark",
|
|
"description": "notify",
|
|
"secret": "s3cr3t",
|
|
"enabled": true
|
|
});
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(json_with_token("POST", "/api/webhooks", body, &token, &csrf))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::CREATED);
|
|
let json = body_json(resp).await;
|
|
let id = json["data"]["id"].as_i64().unwrap();
|
|
assert_eq!(json["data"]["name"], "Team bot");
|
|
// secret must NOT be echoed; has_secret signals presence.
|
|
assert_eq!(json["data"]["has_secret"], true);
|
|
assert!(json["data"].get("secret").is_none(), "secret must never be returned");
|
|
|
|
// list
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(get_with_token("/api/webhooks", &token))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
assert_eq!(body_json(resp).await["data"].as_array().unwrap().len(), 1);
|
|
|
|
// update: rename + clear secret
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(json_with_token(
|
|
"PUT",
|
|
&format!("/api/webhooks/{id}"),
|
|
json!({ "name": "Renamed", "secret": null, "enabled": false }),
|
|
&token,
|
|
&csrf,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["data"]["name"], "Renamed");
|
|
assert_eq!(json["data"]["has_secret"], false);
|
|
assert_eq!(json["data"]["enabled"], false);
|
|
|
|
// delete
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(delete_with_token(&format!("/api/webhooks/{id}"), &token, &csrf))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
|
|
// get after delete → 404
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(get_with_token(&format!("/api/webhooks/{id}"), &token))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn webhook_create_validates_required_fields() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(json_with_token(
|
|
"POST",
|
|
"/api/webhooks",
|
|
json!({ "name": " ", "url": "https://x" }),
|
|
&token,
|
|
&csrf,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn webhook_test_unreachable_url_is_bad_gateway() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
// create a webhook pointing at an unroutable address.
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(json_with_token(
|
|
"POST",
|
|
"/api/webhooks",
|
|
json!({ "name": "bad", "url": "http://127.0.0.1:1/hook", "platform": "lark" }),
|
|
&token,
|
|
&csrf,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
let id = body_json(resp).await["data"]["id"].as_i64().unwrap();
|
|
|
|
// /test invokes the sender; the connection fails → 502 Bad Gateway. This
|
|
// proves the route + sender are wired (we can't reach real Lark in tests).
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(json_with_token(
|
|
"POST",
|
|
&format!("/api/webhooks/{id}/test"),
|
|
json!({}),
|
|
&token,
|
|
&csrf,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::BAD_GATEWAY);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn tag_settings_get_default_and_upsert() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
// unset tag → default (unbound) shape
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(get_with_token("/api/tags/alpha/settings", &token))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["data"]["tag"], "alpha");
|
|
assert!(json["data"]["webhook_id"].is_null());
|
|
|
|
// create a webhook to bind
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(json_with_token(
|
|
"POST",
|
|
"/api/webhooks",
|
|
json!({ "name": "wh", "url": "https://x/hook", "platform": "lark" }),
|
|
&token,
|
|
&csrf,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
let wh_id = body_json(resp).await["data"]["id"].as_i64().unwrap();
|
|
|
|
// bind it to the tag
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(json_with_token(
|
|
"PUT",
|
|
"/api/tags/alpha/settings",
|
|
json!({ "webhook_id": wh_id, "description": "queue alpha" }),
|
|
&token,
|
|
&csrf,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
let json = body_json(resp).await;
|
|
assert_eq!(json["data"]["webhook_id"], wh_id);
|
|
assert_eq!(json["data"]["description"], "queue alpha");
|
|
|
|
// binding a non-existent webhook → 400
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(json_with_token(
|
|
"PUT",
|
|
"/api/tags/alpha/settings",
|
|
json!({ "webhook_id": 999999 }),
|
|
&token,
|
|
&csrf,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn tag_bindings_lists_enabled_autowork_conversations() {
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
// empty initially
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(get_with_token("/api/requirements/tag-bindings", &token))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
assert_eq!(body_json(resp).await["data"].as_array().unwrap().len(), 0);
|
|
|
|
// create a conversation, then enable AutoWork on it for tag "x"
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(json_with_token(
|
|
"POST",
|
|
"/api/conversations",
|
|
json!({ "type": "acp", "name": "Conv X", "extra": { "workspace": "/project" } }),
|
|
&token,
|
|
&csrf,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::CREATED);
|
|
let conv_id = body_json(resp).await["data"]["id"].as_i64().unwrap().to_string();
|
|
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(json_with_token(
|
|
"POST",
|
|
"/api/requirements/autowork",
|
|
json!({ "kind": "conversation", "target_id": conv_id, "enabled": true, "tag": "x" }),
|
|
&token,
|
|
&csrf,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
|
|
// tag-bindings now groups the conversation under "x"
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(get_with_token("/api/requirements/tag-bindings", &token))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(resp.status(), StatusCode::OK);
|
|
let json = body_json(resp).await;
|
|
let groups = json["data"].as_array().unwrap();
|
|
let x = groups.iter().find(|g| g["tag"] == "x").expect("tag x present");
|
|
assert_eq!(x["bindings"].as_array().unwrap().len(), 1);
|
|
assert_eq!(x["bindings"][0]["target_id"], conv_id);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn admin_disable_of_idle_target_is_allowed() {
|
|
// from_admin disable of an idle (not actively executing) target succeeds —
|
|
// the guard only blocks active targets, which require a live in-progress
|
|
// requirement that this lightweight test does not set up.
|
|
let (mut app, services) = build_app().await;
|
|
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
|
|
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(json_with_token(
|
|
"POST",
|
|
"/api/conversations",
|
|
json!({ "type": "acp", "name": "Conv Y", "extra": { "workspace": "/project" } }),
|
|
&token,
|
|
&csrf,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
let conv_id = body_json(resp).await["data"]["id"].as_i64().unwrap().to_string();
|
|
|
|
// enable then admin-disable (idle) → both OK
|
|
for enabled in [true, false] {
|
|
let resp = app
|
|
.clone()
|
|
.oneshot(json_with_token(
|
|
"POST",
|
|
"/api/requirements/autowork",
|
|
json!({ "kind": "conversation", "target_id": conv_id, "enabled": enabled, "tag": "x", "from_admin": true }),
|
|
&token,
|
|
&csrf,
|
|
))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(
|
|
resp.status(),
|
|
StatusCode::OK,
|
|
"enabled={enabled} should be allowed for an idle target"
|
|
);
|
|
}
|
|
}
|