use std::collections::HashMap;
use std::sync::{Arc, Mutex, Once};
use std::time::Duration;
use async_trait::async_trait;
use platform_core::platform::FunctionOptions;
use platform_core::{
overrides, resources, AppConfigReader, AppError, ComposableFunction, EventEnvelope, Platform,
PostOffice,
};
fn setup_config() {
static INIT: Once = Once::new();
INIT.call_once(|| {
resources::prepend_resource_root("tests/resources");
let holding =
std::env::temp_dir().join(format!("mercury-interceptor-test-{}", std::process::id()));
overrides::set("transient.data.store", &holding.display().to_string());
let _ = AppConfigReader::get_instance();
});
}
struct ManualReplier {
platform: Platform,
}
#[async_trait]
impl ComposableFunction for ManualReplier {
async fn handle_event(
&self,
_headers: HashMap<String, String>,
input: EventEnvelope,
_instance: usize,
) -> Result<EventEnvelope, AppError> {
if let (Some(reply_to), Some(cid)) = (input.reply_to(), input.correlation_id()) {
let po = PostOffice::new(&self.platform);
let reply = EventEnvelope::new()
.set_to(reply_to)
.set_correlation_id(cid)
.set_body("manual reply")?;
po.send(reply).await?;
}
EventEnvelope::new().set_body("this must never reach the caller")
}
}
struct SilentSink {
calls: Arc<Mutex<u32>>,
}
#[async_trait]
impl ComposableFunction for SilentSink {
async fn handle_event(
&self,
_headers: HashMap<String, String>,
_input: EventEnvelope,
_instance: usize,
) -> Result<EventEnvelope, AppError> {
*self.calls.lock().expect("calls") += 1;
EventEnvelope::new().set_body("ignored")
}
}
struct Failing;
#[async_trait]
impl ComposableFunction for Failing {
async fn handle_event(
&self,
_headers: HashMap<String, String>,
_input: EventEnvelope,
_instance: usize,
) -> Result<EventEnvelope, AppError> {
Err(AppError::new(409, "interceptor failure"))
}
}
struct Capture {
seen: Arc<Mutex<Vec<String>>>,
}
#[async_trait]
impl ComposableFunction for Capture {
async fn handle_event(
&self,
_headers: HashMap<String, String>,
input: EventEnvelope,
_instance: usize,
) -> Result<EventEnvelope, AppError> {
self.seen
.lock()
.expect("seen")
.push(input.body_as::<String>().unwrap_or_default());
Ok(EventEnvelope::new())
}
}
const INTERCEPTOR: FunctionOptions = FunctionOptions {
zero_traced: false,
interceptor: true,
private: false,
};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn interceptor_replies_manually_and_return_value_is_ignored() {
setup_config();
let platform = Platform::new();
platform
.register_with_options(
"manual.replier",
Arc::new(ManualReplier {
platform: platform.clone(),
}),
1,
INTERCEPTOR,
)
.unwrap();
let po = PostOffice::new(&platform);
let reply = po
.request(
EventEnvelope::new()
.set_to("manual.replier")
.set_body("ping")
.unwrap(),
Duration::from_secs(2),
)
.await
.expect("manual reply");
assert_eq!(reply.body_as::<String>().unwrap(), "manual reply");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn interceptor_success_sends_no_auto_reply() {
setup_config();
let platform = Platform::new();
let calls = Arc::new(Mutex::new(0));
platform
.register_with_options(
"silent.sink",
Arc::new(SilentSink {
calls: calls.clone(),
}),
1,
INTERCEPTOR,
)
.unwrap();
let po = PostOffice::new(&platform);
let result = po
.request(
EventEnvelope::new()
.set_to("silent.sink")
.set_body("ping")
.unwrap(),
Duration::from_millis(500),
)
.await;
assert_eq!(*calls.lock().expect("calls"), 1);
let err = result.expect_err("no auto-reply expected");
assert_eq!(err.status(), 408);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn interceptor_failure_still_reaches_the_caller() {
setup_config();
let platform = Platform::new();
platform
.register_with_options("failing.interceptor", Arc::new(Failing), 1, INTERCEPTOR)
.unwrap();
let po = PostOffice::new(&platform);
let reply = po
.request(
EventEnvelope::new()
.set_to("failing.interceptor")
.set_body("ping")
.unwrap(),
Duration::from_secs(2),
)
.await
.expect("error envelope expected");
assert_eq!(reply.status(), 409);
assert!(reply.has_error());
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn send_later_delivers_after_the_delay() {
setup_config();
let platform = Platform::new();
let seen = Arc::new(Mutex::new(Vec::new()));
platform
.register("timer.capture", Arc::new(Capture { seen: seen.clone() }), 1)
.unwrap();
let po = PostOffice::new(&platform);
po.send_later(
EventEnvelope::new()
.set_to("timer.capture")
.set_body("delayed")
.unwrap(),
Duration::from_millis(100),
);
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(seen.lock().expect("seen").is_empty());
tokio::time::sleep(Duration::from_millis(300)).await;
assert_eq!(seen.lock().expect("seen").clone(), vec!["delayed"]);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn cancel_future_event_stops_delivery() {
setup_config();
let platform = Platform::new();
let seen = Arc::new(Mutex::new(Vec::new()));
platform
.register(
"timer.cancel.capture",
Arc::new(Capture { seen: seen.clone() }),
1,
)
.unwrap();
let po = PostOffice::new(&platform);
let timer_id = po.send_later(
EventEnvelope::new()
.set_to("timer.cancel.capture")
.set_body("never")
.unwrap(),
Duration::from_millis(150),
);
assert!(po.cancel_future_event(&timer_id));
assert!(!po.cancel_future_event(&timer_id));
tokio::time::sleep(Duration::from_millis(400)).await;
assert!(seen.lock().expect("seen").is_empty());
assert!(!po.cancel_future_event("no-such-timer"));
}