KeiSeiKit-1.0/_primitives/_rust/kei-notify-sms/tests/sms_smoke.rs
Parfii-bot a4e667de10 KeiSeiKit-public — clean state
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.
2026-05-01 12:09:03 +08:00

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");
}