mod common;
use common::run_qn;
use serde_json::json;
use wiremock::matchers::{body_partial_json, method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn webhook_payload(id: &str) -> serde_json::Value {
json!({
"id": id,
"name": "wh-test",
"status": "active",
"network": "ethereum-mainnet",
"created_at": "2026-01-01T00:00:00Z",
"template_id": "evmWalletFilter"
})
}
#[tokio::test]
async fn list_webhooks() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/webhooks/rest/v1/webhooks"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [webhook_payload("wh-1")],
"pageInfo": { "limit": 20, "offset": 0, "total": 1 }
})))
.mount(&server)
.await;
let out = run_qn(&server.uri(), &["webhook", "list"]).await;
assert_eq!(out.exit_code, 0, "stderr={}", out.stderr);
}
#[tokio::test]
async fn create_webhook_evm_wallet() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/webhooks/rest/v1/webhooks/template/evmWalletFilter"))
.and(body_partial_json(json!({
"name": "w1",
"network": "ethereum-mainnet",
"destination_attributes": { "url": "https://hook.example/x" },
"templateId": "evmWalletFilter",
"templateArgs": { "wallets": ["0xabc"] }
})))
.respond_with(ResponseTemplate::new(200).set_body_json(webhook_payload("wh-1")))
.mount(&server)
.await;
let out = run_qn(
&server.uri(),
&[
"webhook",
"create",
"--name",
"w1",
"--network",
"ethereum-mainnet",
"--url",
"https://hook.example/x",
"--template",
"evm-wallet",
"--wallet",
"0xabc",
],
)
.await;
assert_eq!(out.exit_code, 0, "stderr={}", out.stderr);
}
#[tokio::test]
async fn create_webhook_evm_contract_events() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path(
"/webhooks/rest/v1/webhooks/template/evmContractEvents",
))
.and(body_partial_json(json!({
"templateId": "evmContractEvents",
"templateArgs": { "contracts": ["0xc1"], "eventHashes": ["0xtopic"] }
})))
.respond_with(ResponseTemplate::new(200).set_body_json(webhook_payload("wh-c")))
.mount(&server)
.await;
let out = run_qn(
&server.uri(),
&[
"webhook",
"create",
"--name",
"w2",
"--network",
"ethereum-mainnet",
"--url",
"https://hook.example/y",
"--template",
"evm-contract-events",
"--contract",
"0xc1",
"--event-hash",
"0xtopic",
],
)
.await;
assert_eq!(out.exit_code, 0, "stderr={}", out.stderr);
}
#[tokio::test]
async fn pause_activate_delete_webhook() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/webhooks/rest/v1/webhooks/wh-1/pause"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/webhooks/rest/v1/webhooks/wh-1/activate"))
.and(body_partial_json(json!({ "startFrom": "latest" })))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
Mock::given(method("DELETE"))
.and(path("/webhooks/rest/v1/webhooks/wh-1"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
assert_eq!(
run_qn(&server.uri(), &["webhook", "pause", "wh-1"])
.await
.exit_code,
0
);
assert_eq!(
run_qn(&server.uri(), &["webhook", "activate", "wh-1"])
.await
.exit_code,
0
);
assert_eq!(
run_qn(&server.uri(), &["webhook", "delete", "wh-1", "--yes"])
.await
.exit_code,
0
);
}
#[tokio::test]
async fn delete_all_webhooks_needs_double_yes() {
let server = MockServer::start().await;
Mock::given(method("DELETE"))
.and(path("/webhooks/rest/v1/webhooks"))
.respond_with(ResponseTemplate::new(200))
.mount(&server)
.await;
let one = run_qn(&server.uri(), &["webhook", "delete-all", "--yes"]).await;
assert_eq!(one.exit_code, 5);
let two = run_qn(&server.uri(), &["webhook", "delete-all", "-y", "-y"]).await;
assert_eq!(two.exit_code, 0, "stderr={}", two.stderr);
}
#[tokio::test]
async fn webhook_enabled_count() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/webhooks/rest/v1/webhooks/enabled_count"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "total": 9 })))
.mount(&server)
.await;
let out = run_qn(&server.uri(), &["webhook", "enabled-count"]).await;
assert_eq!(out.exit_code, 0, "stderr={}", out.stderr);
}
#[tokio::test]
async fn webhook_create_missing_wallets_errors() {
let server = MockServer::start().await;
let _ = server;
let out = run_qn(
"http://127.0.0.1:1", &[
"webhook",
"create",
"--name",
"x",
"--network",
"ethereum-mainnet",
"--url",
"https://hook.example",
"--template",
"evm-wallet",
],
)
.await;
assert_eq!(out.exit_code, 1, "stderr={}", out.stderr);
assert!(out.stderr.contains("--wallet"), "stderr={}", out.stderr);
}
fn nestjs_network_400() -> serde_json::Value {
json!({
"statusCode": 400,
"timestamp": "2026-06-04T14:45:19.326Z",
"path": "/webhooks/rest/v1/webhooks/template/evmWalletFilter",
"message": {
"message": [
"network must be one of the following values: ethereum-mainnet, ethereum-sepolia, solana-mainnet, solana-devnet, base-mainnet, base-sepolia, polygon-mainnet, polygon-amoy"
],
"error": "Bad Request",
"statusCode": 400
}
})
}
#[tokio::test]
async fn create_webhook_400_renders_bullets_with_typo_suggestion() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/webhooks/rest/v1/webhooks/template/evmWalletFilter"))
.respond_with(ResponseTemplate::new(400).set_body_json(nestjs_network_400()))
.mount(&server)
.await;
let out = run_qn(
&server.uri(),
&[
"webhook",
"create",
"--name",
"w1",
"--network",
"ethereum-mainnetsds",
"--url",
"https://hook.example/x",
"--template",
"evm-wallet",
"--wallet",
"0xabc",
],
)
.await;
assert_eq!(out.exit_code, 2, "stderr={}", out.stderr);
assert!(
out.stderr.contains("invalid request"),
"stderr={}",
out.stderr
);
assert!(
out.stderr.contains("network must be one of"),
"stderr={}",
out.stderr
);
assert!(
out.stderr.contains("did you mean 'ethereum-mainnet'"),
"stderr={}",
out.stderr
);
assert!(
out.stderr.contains("qn chain list"),
"stderr={}",
out.stderr
);
}
#[tokio::test]
async fn create_webhook_400_far_typo_no_suggestion() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/webhooks/rest/v1/webhooks/template/evmWalletFilter"))
.respond_with(ResponseTemplate::new(400).set_body_json(nestjs_network_400()))
.mount(&server)
.await;
let out = run_qn(
&server.uri(),
&[
"webhook",
"create",
"--name",
"w1",
"--network",
"sfjla",
"--url",
"https://hook.example/x",
"--template",
"evm-wallet",
"--wallet",
"0xabc",
],
)
.await;
assert_eq!(out.exit_code, 2, "stderr={}", out.stderr);
assert!(
out.stderr.contains("network must be one of"),
"stderr={}",
out.stderr
);
assert!(
!out.stderr.contains("did you mean"),
"should not suggest for 'sfjla'; stderr={}",
out.stderr
);
}