Single-commit clean baseline after security scrub of niche-tells, project codenames, internal jargon, and contributor-email leaks. Contents: - 100 Rust crates (_primitives/_rust/) - 37 agent manifests (_manifests/) + generated specs (_generated/) - 67 user-invocable skills (skills/) - 33 hooks (hooks/) - Composition blocks (_blocks/) - Documentation (docs/, README.md) - TS adapter packages (_ts_packages/) - Assembler (_assembler/) - Roles (_roles/) - Templates (_templates/) - Forgejo CI (.forgejo/) Author: Denis Parfionovich <info@greendragon.info> License: see LICENSE.
178 lines
5.6 KiB
Rust
178 lines
5.6 KiB
Rust
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright 2026 <author org>
|
|
//!
|
|
//! Twilio REST integration tests against a `wiremock` stub. No live
|
|
//! HTTP — every assertion is local to the test process.
|
|
|
|
use base64::engine::general_purpose::STANDARD as B64;
|
|
use base64::Engine as _;
|
|
use kei_notify_sms::SmsChannel;
|
|
use kei_runtime_core::traits::notify::{
|
|
Notification, NotifyChannel, NotifySeverity,
|
|
};
|
|
use kei_runtime_core::{DnaBuilder, HasDna};
|
|
use serde_json::json;
|
|
use wiremock::matchers::{header, method, path};
|
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
|
|
|
const SID: &str = "ACtest_sid_0123456789abcdef";
|
|
const TOKEN: &str = "test_auth_token_xxx";
|
|
const FROM: &str = "+15005550006";
|
|
const TO: &str = "+15005550010";
|
|
|
|
fn channel(server: &MockServer) -> SmsChannel {
|
|
SmsChannel::with_config(server.uri(), SID, TOKEN, FROM, TO, None).unwrap()
|
|
}
|
|
|
|
fn notif(sev: NotifySeverity, subject: &str, body: &str) -> Notification {
|
|
let dna = DnaBuilder::new("notification")
|
|
.cap("NF")
|
|
.scope("test")
|
|
.body(b"smoke")
|
|
.build()
|
|
.unwrap();
|
|
let parent = DnaBuilder::new("primitive")
|
|
.cap("PR")
|
|
.scope("test-parent")
|
|
.body(b"parent")
|
|
.build()
|
|
.unwrap();
|
|
Notification {
|
|
dna,
|
|
parent_dna: parent,
|
|
subject: subject.into(),
|
|
body_text: body.into(),
|
|
body_html: None,
|
|
severity: sev,
|
|
tags: vec![],
|
|
}
|
|
}
|
|
|
|
fn endpoint_path() -> String {
|
|
format!("/2010-04-01/Accounts/{SID}/Messages.json")
|
|
}
|
|
|
|
fn expected_basic_header() -> String {
|
|
let raw = format!("{SID}:{TOKEN}");
|
|
format!("Basic {}", B64.encode(raw.as_bytes()))
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_201_success() {
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("POST"))
|
|
.and(path(endpoint_path()))
|
|
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
|
"sid": "SMxxxxxxxxxxxxxxxx",
|
|
"status": "queued"
|
|
})))
|
|
.expect(1)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let ch = channel(&server);
|
|
let n = notif(NotifySeverity::Warn, "alert", "disk 92%");
|
|
ch.send(&n).await.expect("201 should be Ok");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn send_400_returns_api_error() {
|
|
let server = MockServer::start().await;
|
|
Mock::given(method("POST"))
|
|
.and(path(endpoint_path()))
|
|
.respond_with(ResponseTemplate::new(400).set_body_json(json!({
|
|
"code": 21211,
|
|
"message": "Invalid 'To' Phone Number",
|
|
"more_info": "https://www.twilio.com/docs/errors/21211",
|
|
"status": 400
|
|
})))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let ch = channel(&server);
|
|
let n = notif(NotifySeverity::Error, "fail", "boom");
|
|
let err = ch.send(&n).await.expect_err("400 must surface as Err");
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("21211") || msg.contains("Invalid"),
|
|
"expected twilio code/message in error, got: {msg}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn info_severity_dropped_by_filter() {
|
|
// The trait dispatcher consults `min_severity()` to gate delivery.
|
|
// SmsChannel overrides to `Warn`, so `Info` is below the floor and
|
|
// would be dropped. We assert the predicate directly.
|
|
let server = MockServer::start().await;
|
|
let ch = channel(&server);
|
|
assert_eq!(ch.min_severity(), NotifySeverity::Warn);
|
|
|
|
// Demonstrate the filter contract that an upstream dispatcher would
|
|
// enforce. `NotifySeverity` doesn't impl Ord, so the gate is an
|
|
// explicit allow-list per channel.
|
|
let allowed = |sev: NotifySeverity| -> bool {
|
|
match (ch.min_severity(), sev) {
|
|
(NotifySeverity::Warn, NotifySeverity::Info)
|
|
| (NotifySeverity::Warn, NotifySeverity::Success) => false,
|
|
(NotifySeverity::Warn, _) => true,
|
|
_ => true,
|
|
}
|
|
};
|
|
assert!(!allowed(NotifySeverity::Info), "Info must be filtered");
|
|
assert!(!allowed(NotifySeverity::Success), "Success must be filtered");
|
|
assert!(allowed(NotifySeverity::Warn));
|
|
assert!(allowed(NotifySeverity::Error));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn warn_severity_delivered() {
|
|
let server = MockServer::start().await;
|
|
// No body matcher — wiremock 0.6 form-body matching varies across
|
|
// patch versions; we verify delivery by 201 + path + method only.
|
|
Mock::given(method("POST"))
|
|
.and(path(endpoint_path()))
|
|
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
|
"sid": "SMwarn_ok",
|
|
"status": "queued"
|
|
})))
|
|
.expect(1)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let ch = channel(&server);
|
|
let n = notif(NotifySeverity::Warn, "boot", "rebooting");
|
|
ch.send(&n).await.expect("warn should reach the wire");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn dna_has_sm_cap() {
|
|
let server = MockServer::start().await;
|
|
let ch = channel(&server);
|
|
let caps = ch.dna().caps();
|
|
assert!(caps.contains("SM"), "expected SM in caps, got {caps}");
|
|
assert!(caps.contains("PR"));
|
|
assert!(caps.contains("AP"));
|
|
assert_eq!(ch.channel_name(), "sms");
|
|
assert!(!ch.supports_batching());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn http_basic_auth_header_present() {
|
|
let server = MockServer::start().await;
|
|
let expected = expected_basic_header();
|
|
Mock::given(method("POST"))
|
|
.and(path(endpoint_path()))
|
|
.and(header("authorization", expected.as_str()))
|
|
.respond_with(ResponseTemplate::new(201).set_body_json(json!({
|
|
"sid": "SMauth_ok",
|
|
"status": "queued"
|
|
})))
|
|
.expect(1)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let ch = channel(&server);
|
|
let n = notif(NotifySeverity::Warn, "auth", "ping");
|
|
ch.send(&n).await.expect("basic-auth header must match");
|
|
}
|