use dataflow_rs::datalogic_rs;
use std::sync::Arc;
use serde_json::{Value, json};
use super::ChannelRuntimeConfig;
use crate::connector::cache_backend::CacheBackend;
use crate::errors::OrionError;
use crate::metrics;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Transport {
HttpSync,
HttpAsync,
Kafka,
ChannelCall,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GuardSet {
pub auth: bool,
pub origin_allow_list: bool,
pub rate_limit: bool,
pub validation: bool,
pub deduplication: bool,
pub response_cache: bool,
pub backpressure: bool,
}
impl Transport {
pub const fn guards(self) -> GuardSet {
match self {
Transport::HttpSync => GuardSet {
auth: true,
origin_allow_list: true,
rate_limit: true,
validation: true,
deduplication: true,
response_cache: true,
backpressure: true,
},
Transport::HttpAsync => GuardSet {
auth: true,
origin_allow_list: true,
rate_limit: true,
validation: true,
deduplication: true,
response_cache: false,
backpressure: true,
},
Transport::Kafka => GuardSet {
auth: false,
origin_allow_list: false,
rate_limit: true,
validation: true,
deduplication: true,
response_cache: false,
backpressure: true,
},
Transport::ChannelCall => GuardSet {
auth: false,
origin_allow_list: false,
rate_limit: true,
validation: true,
deduplication: false,
response_cache: false,
backpressure: true,
},
}
}
}
pub type HeaderLookup<'a> = &'a (dyn Fn(&str) -> Option<String> + Send + Sync);
pub struct GuardRequest<'a> {
pub transport: Transport,
pub channel: &'a str,
pub runtime: &'a Option<Arc<ChannelRuntimeConfig>>,
pub data: &'a Value,
pub metadata: &'a Value,
pub datalogic: &'a datalogic_rs::Engine,
pub origin: Option<&'a str>,
pub caller_identity: &'a str,
pub header: HeaderLookup<'a>,
pub raw_body: Option<&'a [u8]>,
pub dedup_key_fallback: Option<&'a str>,
pub dedup_owner: Option<&'a str>,
pub default_timeout_ms: Option<u64>,
pub max_timeout_ms: Option<u64>,
}
pub struct Admission {
pub backpressure_permit: Option<tokio::sync::OwnedSemaphorePermit>,
pub cache_store: Option<CacheStoreCtx>,
pub timeout_ms: Option<u64>,
pub dedup_claim: Option<DedupClaim>,
}
pub struct DedupClaim {
store: Arc<dyn CacheBackend>,
key: String,
window_secs: u64,
}
const DEDUP_SETTLED: &str = "settled";
impl DedupClaim {
pub async fn confirm(self) {
if let Err(e) = self
.store
.set_ex(&self.key, DEDUP_SETTLED, self.window_secs)
.await
{
tracing::warn!(
key = %self.key,
error = %e,
"Could not mark the idempotency key settled; a redelivery of this message would be reprocessed"
);
}
}
pub async fn release(self) {
if let Err(e) = self.store.remove(&self.key).await {
tracing::warn!(
key = %self.key,
error = %e,
"Could not release the idempotency key after an unsettled delivery"
);
}
}
}
pub enum GuardVerdict {
Admitted(Admission),
CacheHit(String),
}
pub async fn apply_guards(req: GuardRequest<'_>) -> Result<GuardVerdict, OrionError> {
let set = req.transport.guards();
if set.rate_limit {
check_rate_limit(
req.channel,
req.runtime,
req.datalogic,
req.caller_identity,
req.header,
)
.await?;
}
if set.auth {
check_auth(req.channel, req.runtime, req.header, req.raw_body)?;
}
if set.origin_allow_list {
check_allowed_origin(req.channel, req.runtime, req.origin)?;
}
if set.validation {
validate_input(
req.channel,
req.runtime,
req.data,
req.metadata,
req.datalogic,
)?;
}
let dedup_claim = if set.deduplication {
check_deduplication(
req.channel,
req.runtime,
req.header,
req.dedup_key_fallback,
req.dedup_owner,
)
.await?
} else {
None
};
let cache_store = if set.response_cache {
match check_response_cache(req.channel, req.data, req.metadata, req.runtime).await {
CacheLookup::Hit(body) => return Ok(GuardVerdict::CacheHit(body)),
CacheLookup::Miss(ctx) => ctx,
}
} else {
None
};
let backpressure_permit = if set.backpressure {
match acquire_backpressure(req.channel, req.runtime) {
Ok(permit) => permit,
Err(e) => {
if let Some(claim) = dedup_claim {
claim.release().await;
}
return Err(e);
}
}
} else {
None
};
Ok(GuardVerdict::Admitted(Admission {
backpressure_permit,
cache_store,
timeout_ms: effective_timeout_ms(req.runtime, req.default_timeout_ms, req.max_timeout_ms),
dedup_claim,
}))
}
pub async fn admit(req: GuardRequest<'_>) -> Result<Admission, OrionError> {
let transport = req.transport;
match apply_guards(req).await? {
GuardVerdict::Admitted(admission) => Ok(admission),
GuardVerdict::CacheHit(_) => Err(OrionError::internal(format!(
"{transport:?} does not enable the response cache"
))),
}
}
pub fn effective_timeout_ms(
runtime: &Option<Arc<ChannelRuntimeConfig>>,
default_timeout_ms: Option<u64>,
max_timeout_ms: Option<u64>,
) -> Option<u64> {
let resolved = runtime
.as_ref()
.and_then(|c| c.parsed_config.timeout_ms)
.or(default_timeout_ms)?;
Some(match max_timeout_ms {
Some(max) => resolved.min(max),
None => resolved,
})
}
fn is_truthy(val: &Value) -> bool {
match val {
Value::Null => false,
Value::Bool(b) => *b,
Value::Number(n) => n.as_f64().is_some_and(|f| f != 0.0),
Value::String(s) => !s.is_empty(),
Value::Array(a) => !a.is_empty(),
Value::Object(_) => true,
}
}
fn check_auth(
channel: &str,
channel_config: &Option<Arc<ChannelRuntimeConfig>>,
header: HeaderLookup<'_>,
raw_body: Option<&[u8]>,
) -> Result<(), OrionError> {
let Some(cfg) = channel_config else {
return Ok(());
};
let Some(ref auth) = cfg.auth else {
return Ok(());
};
auth.authenticate(header, raw_body).inspect_err(|_| {
metrics::record_message(channel, "unauthorized");
tracing::warn!(channel = %channel, "Channel authentication failed");
})
}
fn check_allowed_origin(
channel: &str,
channel_config: &Option<Arc<ChannelRuntimeConfig>>,
origin: Option<&str>,
) -> Result<(), OrionError> {
if let Some(cfg) = channel_config
&& let Some(allowed_origins) = cfg.parsed_config.allowed_origins()
&& let Some(origin) = origin
&& !allowed_origins.iter().any(|o| o == "*" || o == origin)
{
return Err(OrionError::Forbidden(format!(
"Origin '{origin}' is not allowed for channel '{channel}'"
)));
}
Ok(())
}
async fn check_rate_limit(
channel: &str,
channel_config: &Option<Arc<ChannelRuntimeConfig>>,
datalogic: &datalogic_rs::Engine,
caller_identity: &str,
header: HeaderLookup<'_>,
) -> Result<(), OrionError> {
let Some(cfg) = channel_config else {
return Ok(());
};
let Some(ref limiter) = cfg.rate_limiter else {
return Ok(());
};
let key = if let Some(ref compiled) = cfg.rate_limit_key_logic {
let context = rate_limit_context(caller_identity, channel, header);
match datalogic
.session()
.eval_into::<serde_json::Value, _>(compiled, &context)
{
Ok(val) => val
.as_str()
.map(str::to_string)
.unwrap_or_else(|| serde_json::to_string(&val).unwrap_or_default()),
Err(e) => {
tracing::warn!(
channel = %channel,
error = %e,
"rate_limit.key_logic evaluation failed; rejecting request"
);
metrics::record_rate_limit_rejected(channel);
return Err(OrionError::RateLimitKeyUnavailable(
"Too many requests".to_string(),
));
}
}
} else {
caller_identity.to_string()
};
let policy = cfg
.parsed_config
.rate_limit
.as_ref()
.map(|rl| rl.on_backend_error)
.unwrap_or_default();
match limiter.check(key).await {
Ok(true) => Ok(()),
Ok(false) => {
metrics::record_rate_limit_rejected(channel);
Err(OrionError::RateLimited("Too many requests".to_string()))
}
Err(e) => {
metrics::record_error("rate_limit_backend");
match policy {
crate::channel::BackendErrorPolicy::Allow => {
tracing::warn!(
channel = %channel,
error = %e,
"Rate-limit backend error; failing open (request allowed)"
);
Ok(())
}
crate::channel::BackendErrorPolicy::Deny => {
tracing::warn!(
channel = %channel,
error = %e,
"Rate-limit backend error; failing closed (request refused)"
);
metrics::record_rate_limit_rejected(channel);
Err(OrionError::ServiceUnavailable(format!(
"Channel '{channel}' cannot check its rate limit: the backend is \
unavailable and the channel is configured to fail closed"
)))
}
}
}
}
}
fn rate_limit_context(caller_identity: &str, channel: &str, header: HeaderLookup<'_>) -> Value {
const COMMON_HEADERS: &[&str] = &[
"authorization",
"x-api-key",
"x-forwarded-for",
"x-real-ip",
"user-agent",
"content-type",
"origin",
"x-tenant-id",
];
let mut headers = serde_json::Map::with_capacity(COMMON_HEADERS.len());
for &name in COMMON_HEADERS {
if let Some(value) = header(name) {
headers.insert(name.to_string(), Value::String(value));
}
}
json!({
"client_ip": caller_identity,
"channel": channel,
"headers": headers,
})
}
fn validate_input(
channel: &str,
channel_config: &Option<Arc<ChannelRuntimeConfig>>,
data: &Value,
metadata: &Value,
datalogic: &datalogic_rs::Engine,
) -> Result<(), OrionError> {
if let Some(cfg) = channel_config
&& let Some(ref compiled) = cfg.validation_logic
{
let context = json!({ "data": data, "metadata": metadata });
match datalogic
.session()
.eval_into::<serde_json::Value, _>(compiled, &context)
{
Ok(result) => {
if !is_truthy(&result) {
return Err(OrionError::validation(
"Input validation failed".to_string(),
));
}
}
Err(e) => {
tracing::warn!(channel = %channel, error = %e, "validation_logic evaluation failed, rejecting");
return Err(OrionError::validation(
"Input validation failed".to_string(),
));
}
}
}
Ok(())
}
async fn check_deduplication(
channel: &str,
channel_config: &Option<Arc<ChannelRuntimeConfig>>,
header: HeaderLookup<'_>,
key_fallback: Option<&str>,
owner: Option<&str>,
) -> Result<Option<DedupClaim>, OrionError> {
let Some(cfg) = channel_config else {
return Ok(None);
};
let Some(ref dedup) = cfg.parsed_config.deduplication else {
return Ok(None);
};
let Some(ref store) = cfg.dedup_store else {
return Ok(None);
};
let Some(key) = header(&dedup.header).or_else(|| key_fallback.map(str::to_string)) else {
return Ok(None);
};
let window = dedup.window_secs.unwrap_or(300);
let scoped_key = format!("dedup:{channel}:{key}");
let one_shot;
let owner = match owner {
Some(owner) => owner,
None => {
one_shot = uuid::Uuid::new_v4().simple().to_string();
one_shot.as_str()
}
};
let holder = match store.claim_dedup_key(&scoped_key, owner, window).await {
Ok(holder) => holder,
Err(e) => {
metrics::record_error("dedup_backend");
match dedup.on_backend_error {
crate::channel::BackendErrorPolicy::Allow => {
tracing::warn!(
channel = %channel,
error = %e,
header = %dedup.header,
"Dedup backend error; failing open (request allowed without dedup check)"
);
return Ok(None);
}
crate::channel::BackendErrorPolicy::Deny => {
tracing::warn!(
channel = %channel,
error = %e,
header = %dedup.header,
"Dedup backend error; failing closed (request refused)"
);
return Err(OrionError::ServiceUnavailable(format!(
"Channel '{channel}' cannot verify the idempotency key: the \
deduplication backend is unavailable and the channel is \
configured to fail closed"
)));
}
}
}
};
match holder {
None => {}
Some(ref held) if held == owner => {
tracing::debug!(
channel = %channel,
key = %key,
"Redelivery of an unsettled message; the idempotency claim is its own"
);
}
Some(_) => {
return Err(OrionError::Conflict(format!(
"Duplicate request: idempotency key '{key}' already seen"
)));
}
}
Ok(Some(DedupClaim {
store: store.clone(),
key: scoped_key,
window_secs: window,
}))
}
fn acquire_backpressure(
channel: &str,
channel_config: &Option<Arc<ChannelRuntimeConfig>>,
) -> Result<Option<tokio::sync::OwnedSemaphorePermit>, OrionError> {
if let Some(cfg) = channel_config
&& let Some(ref semaphore) = cfg.backpressure_semaphore
{
match semaphore.clone().try_acquire_owned() {
Ok(permit) => Ok(Some(permit)),
Err(_) => {
metrics::record_error("backpressure");
Err(OrionError::ServiceUnavailable(format!(
"Channel '{channel}' is at capacity"
)))
}
}
} else {
Ok(None)
}
}
use sha2::{Digest, Sha256};
fn resolve_key_field<'a>(data: &'a Value, field: &str) -> Option<&'a Value> {
fn walk<'a>(mut cur: &'a Value, path: &str) -> Option<&'a Value> {
for segment in path.split('.') {
if segment.is_empty() {
return None;
}
cur = cur.get(segment)?;
}
Some(cur)
}
if let Some(v) = data.get(field) {
return Some(v);
}
if !field.contains('.') {
return None;
}
walk(data, field).or_else(|| field.strip_prefix("data.").and_then(|p| walk(data, p)))
}
fn compute_cache_key(
channel: &str,
data: &Value,
metadata: &Value,
cache_cfg: &crate::channel::ChannelCacheConfig,
) -> Option<String> {
let mut h = Sha256::new();
fn feed(h: &mut Sha256, bytes: &[u8]) {
h.update((bytes.len() as u64).to_be_bytes());
h.update(bytes);
}
feed(
&mut h,
metadata
.get("http_method")
.and_then(Value::as_str)
.unwrap_or("")
.as_bytes(),
);
feed_object_sorted(&mut h, metadata.get("params"));
feed_object_sorted(&mut h, metadata.get("query"));
if let Some(ref fields) = cache_cfg.cache_key_fields {
let mut resolved = 0usize;
for f in fields {
feed(&mut h, f.as_bytes());
match resolve_key_field(data, f) {
Some(v) => {
resolved += 1;
h.update([1u8]);
feed(&mut h, &serde_json::to_vec(v).unwrap_or_default());
}
None => h.update([0u8]),
}
}
if resolved == 0 {
return None;
}
} else {
feed(&mut h, &serde_json::to_vec(data).unwrap_or_default());
};
let digest = h.finalize();
Some(format!("cache:{channel}:{}", hex::encode(&digest[..16])))
}
fn feed_object_sorted(h: &mut Sha256, v: Option<&Value>) {
let Some(Value::Object(map)) = v else {
h.update([0u8]);
return;
};
h.update([1u8]);
h.update((map.len() as u64).to_be_bytes());
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_unstable();
for k in keys {
h.update((k.len() as u64).to_be_bytes());
h.update(k.as_bytes());
let bytes = serde_json::to_vec(&map[k.as_str()]).unwrap_or_default();
h.update((bytes.len() as u64).to_be_bytes());
h.update(&bytes);
}
}
pub type CacheStoreCtx = (String, Arc<dyn CacheBackend>, u64);
enum CacheLookup {
Hit(String),
Miss(Option<CacheStoreCtx>),
}
async fn check_response_cache(
channel: &str,
data: &Value,
metadata: &Value,
channel_config: &Option<Arc<ChannelRuntimeConfig>>,
) -> CacheLookup {
let Some(cfg) = channel_config else {
return CacheLookup::Miss(None);
};
let Some(ref cache_cfg) = cfg.parsed_config.cache else {
return CacheLookup::Miss(None);
};
if !cache_cfg.enabled {
return CacheLookup::Miss(None);
}
let Some(ref cache) = cfg.response_cache else {
return CacheLookup::Miss(None);
};
let Some(key) = compute_cache_key(channel, data, metadata, cache_cfg) else {
tracing::warn!(
channel = %channel,
fields = ?cache_cfg.cache_key_fields,
"No cache_key_fields resolved against the request payload; bypassing the \
response cache. Field names are literal payload keys or dotted paths \
(`user.id`, or `data.user_id` for a top-level `user_id`)."
);
return CacheLookup::Miss(None);
};
match cache.get(&key).await {
Ok(Some(cached)) => {
metrics::record_cache_hit(channel);
CacheLookup::Hit(cached)
}
_ => {
metrics::record_cache_miss(channel);
CacheLookup::Miss(Some((
key,
cache.clone(),
cache_cfg.ttl_secs.unwrap_or(300),
)))
}
}
}
#[cfg(test)]
mod tests {
use dataflow_rs::datalogic_rs;
use std::sync::Arc;
use async_trait::async_trait;
use serde_json::json;
use super::{
Admission, GuardRequest, GuardVerdict, HeaderLookup, Transport, Value, apply_guards,
effective_timeout_ms,
};
use crate::channel::registry::EffectiveTraceConfig;
use crate::channel::{
BackendErrorPolicy, ChannelConfig, ChannelRuntimeConfig, DeduplicationConfig,
};
use crate::config::TraceStorageConfig;
use crate::connector::cache_backend::CacheBackend;
use crate::errors::OrionError;
use crate::storage::models::Channel;
enum StubOutcome {
New,
Duplicate,
BackendError,
}
struct StubDedupBackend {
outcome: StubOutcome,
}
#[async_trait]
impl CacheBackend for StubDedupBackend {
async fn get(&self, _key: &str) -> Result<Option<String>, OrionError> {
Ok(None)
}
async fn set(&self, _key: &str, _value: &str) -> Result<(), OrionError> {
Ok(())
}
async fn set_ex(&self, _key: &str, _value: &str, _ttl: u64) -> Result<(), OrionError> {
Ok(())
}
async fn remove(&self, _key: &str) -> Result<(), OrionError> {
Ok(())
}
async fn claim_dedup_key(
&self,
_key: &str,
_owner: &str,
_window: u64,
) -> Result<Option<String>, OrionError> {
match self.outcome {
StubOutcome::New => Ok(None),
StubOutcome::Duplicate => Ok(Some(super::DEDUP_SETTLED.to_string())),
StubOutcome::BackendError => {
Err(OrionError::internal("dedup backend down".to_string()))
}
}
}
}
#[derive(Default)]
struct InMemoryDedupBackend {
held: std::sync::Mutex<std::collections::HashMap<String, String>>,
}
#[async_trait]
impl CacheBackend for InMemoryDedupBackend {
async fn get(&self, key: &str) -> Result<Option<String>, OrionError> {
Ok(self
.held
.lock()
.expect("test lock poisoned")
.get(key)
.cloned())
}
async fn set(&self, key: &str, value: &str) -> Result<(), OrionError> {
self.held
.lock()
.expect("test lock poisoned")
.insert(key.to_string(), value.to_string());
Ok(())
}
async fn set_ex(&self, key: &str, value: &str, _ttl: u64) -> Result<(), OrionError> {
self.set(key, value).await
}
async fn remove(&self, key: &str) -> Result<(), OrionError> {
self.held.lock().expect("test lock poisoned").remove(key);
Ok(())
}
async fn claim_dedup_key(
&self,
key: &str,
owner: &str,
_window: u64,
) -> Result<Option<String>, OrionError> {
let mut held = self.held.lock().expect("test lock poisoned");
match held.get(key) {
Some(holder) => Ok(Some(holder.clone())),
None => {
held.insert(key.to_string(), owner.to_string());
Ok(None)
}
}
}
}
struct CapturingDedupBackend {
seen: Arc<std::sync::Mutex<Vec<String>>>,
}
#[async_trait]
impl CacheBackend for CapturingDedupBackend {
async fn get(&self, _key: &str) -> Result<Option<String>, OrionError> {
Ok(None)
}
async fn set(&self, _key: &str, _value: &str) -> Result<(), OrionError> {
Ok(())
}
async fn set_ex(&self, _key: &str, _value: &str, _ttl: u64) -> Result<(), OrionError> {
Ok(())
}
async fn remove(&self, _key: &str) -> Result<(), OrionError> {
Ok(())
}
async fn claim_dedup_key(
&self,
key: &str,
_owner: &str,
_window: u64,
) -> Result<Option<String>, OrionError> {
self.seen
.lock()
.expect("test lock poisoned")
.push(key.to_string());
Ok(None)
}
}
struct AlwaysHitCache;
#[async_trait]
impl CacheBackend for AlwaysHitCache {
async fn get(&self, _key: &str) -> Result<Option<String>, OrionError> {
Ok(Some(r#"{"cached":true}"#.to_string()))
}
async fn set(&self, _key: &str, _value: &str) -> Result<(), OrionError> {
Ok(())
}
async fn set_ex(&self, _key: &str, _value: &str, _ttl: u64) -> Result<(), OrionError> {
Ok(())
}
async fn remove(&self, _key: &str) -> Result<(), OrionError> {
Ok(())
}
async fn claim_dedup_key(
&self,
_key: &str,
_owner: &str,
_window: u64,
) -> Result<Option<String>, OrionError> {
Ok(None)
}
}
struct FailingLimiter;
#[async_trait]
impl crate::channel::RateLimitBackend for FailingLimiter {
async fn check(&self, _key: String) -> Result<bool, OrionError> {
Err(OrionError::internal("backend down".to_string()))
}
}
struct Runtime {
parsed_config: ChannelConfig,
rate_limiter: Option<Arc<dyn crate::channel::RateLimitBackend>>,
rate_limit_key_logic: Option<datalogic_rs::Logic>,
validation_logic: Option<datalogic_rs::Logic>,
backpressure_semaphore: Option<Arc<tokio::sync::Semaphore>>,
dedup_store: Option<Arc<dyn CacheBackend>>,
response_cache: Option<Arc<dyn CacheBackend>>,
auth: Option<crate::channel::auth::CompiledAuth>,
}
impl Runtime {
fn new() -> Self {
Self {
parsed_config: ChannelConfig::default(),
rate_limiter: None,
rate_limit_key_logic: None,
validation_logic: None,
backpressure_semaphore: None,
auth: None,
dedup_store: None,
response_cache: None,
}
}
fn dedup(mut self, store: Arc<dyn CacheBackend>, policy: BackendErrorPolicy) -> Self {
self.parsed_config.deduplication = Some(DeduplicationConfig {
header: "idempotency-key".to_string(),
window_secs: Some(60),
connector: None,
on_backend_error: policy,
});
self.dedup_store = Some(store);
self
}
fn origins(mut self, origins: &[&str]) -> Self {
self.parsed_config.origin_allow_list =
Some(origins.iter().map(|o| o.to_string()).collect());
self
}
fn limiter(
mut self,
backend: Arc<dyn crate::channel::RateLimitBackend>,
policy: BackendErrorPolicy,
) -> Self {
self.parsed_config.rate_limit = Some(crate::channel::ChannelRateLimitConfig {
requests_per_second: 1,
burst: Some(1),
key_logic: None,
on_backend_error: policy,
});
self.rate_limiter = Some(backend);
self
}
fn key_logic(mut self, engine: &datalogic_rs::Engine, logic: serde_json::Value) -> Self {
self.rate_limit_key_logic = Some(engine.compile(&logic).expect("test logic compiles"));
self
}
fn validation(mut self, engine: &datalogic_rs::Engine, logic: serde_json::Value) -> Self {
self.validation_logic = Some(engine.compile(&logic).expect("test logic compiles"));
self
}
fn backpressure(mut self, permits: usize) -> Self {
self.parsed_config.backpressure = Some(crate::channel::BackpressureConfig {
max_concurrent_per_node: permits,
});
self.backpressure_semaphore = Some(Arc::new(tokio::sync::Semaphore::new(permits)));
self
}
fn cache(mut self, backend: Arc<dyn CacheBackend>) -> Self {
self.parsed_config.cache = Some(crate::channel::ChannelCacheConfig {
enabled: true,
ttl_secs: Some(60),
cache_key_fields: None,
connector: None,
});
self.response_cache = Some(backend);
self
}
fn timeout_ms(mut self, ms: u64) -> Self {
self.parsed_config.timeout_ms = Some(ms);
self
}
async fn api_key(mut self, key: &str) -> Self {
let cfg = crate::channel::config::ChannelAuthConfig {
mode: crate::channel::config::AuthMode::ApiKey,
keys: Some(vec![key.to_string()]),
header: Some("X-API-Key".to_string()),
scheme: None,
secret: None,
signature_prefix: None,
};
self.auth = Some(
crate::channel::auth::CompiledAuth::compile(&cfg)
.await
.expect("test auth compiles"),
);
self.parsed_config.auth = Some(cfg);
self
}
fn build(self) -> Option<Arc<ChannelRuntimeConfig>> {
let now = chrono::Utc::now().naive_utc();
Some(Arc::new(ChannelRuntimeConfig {
channel: Channel {
tags_json: "[]".to_string(),
channel_id: "ch_test".to_string(),
version: 1,
name: "test-channel".to_string(),
description: None,
channel_type: "sync".to_string(),
protocol: "rest".to_string(),
methods_json: None,
route_pattern: None,
topic: None,
consumer_group: None,
transport_config_json: "{}".to_string(),
workflow_id: None,
config_json: "{}".to_string(),
status: "active".to_string(),
priority: 0,
created_at: now,
updated_at: now,
},
parsed_config: self.parsed_config,
rate_limiter: self.rate_limiter,
rate_limit_key_logic: self.rate_limit_key_logic,
validation_logic: self.validation_logic,
backpressure_semaphore: self.backpressure_semaphore,
dedup_store: self.dedup_store,
response_cache: self.response_cache,
trace_storage: EffectiveTraceConfig::resolve(&TraceStorageConfig::default(), None),
auth: self.auth,
}))
}
}
fn dedup_runtime(outcome: StubOutcome) -> Option<Arc<ChannelRuntimeConfig>> {
dedup_runtime_with_policy(outcome, BackendErrorPolicy::Allow)
}
fn dedup_runtime_with_policy(
outcome: StubOutcome,
policy: BackendErrorPolicy,
) -> Option<Arc<ChannelRuntimeConfig>> {
Runtime::new()
.dedup(Arc::new(StubDedupBackend { outcome }), policy)
.build()
}
fn idempotency_lookup(name: &str) -> Option<String> {
(name == "idempotency-key").then(|| "token-1".to_string())
}
fn no_headers(_name: &str) -> Option<String> {
None
}
const IDEMPOTENCY: HeaderLookup<'static> = &idempotency_lookup;
const NO_HEADERS: HeaderLookup<'static> = &no_headers;
fn engine() -> datalogic_rs::Engine {
datalogic_rs::Engine::new()
}
fn request<'a>(
transport: Transport,
runtime: &'a Option<Arc<ChannelRuntimeConfig>>,
datalogic: &'a datalogic_rs::Engine,
data: &'a Value,
metadata: &'a Value,
) -> GuardRequest<'a> {
GuardRequest {
transport,
channel: "orders",
runtime,
data,
metadata,
datalogic,
origin: None,
caller_identity: "10.0.0.1",
header: NO_HEADERS,
raw_body: None,
dedup_key_fallback: None,
dedup_owner: None,
default_timeout_ms: None,
max_timeout_ms: None,
}
}
fn admitted(verdict: GuardVerdict) -> Option<Admission> {
match verdict {
GuardVerdict::Admitted(a) => Some(a),
GuardVerdict::CacheHit(_) => None,
}
}
#[tokio::test]
async fn the_guard_matrix_is_what_the_docs_claim() {
let sync = Transport::HttpSync.guards();
let submit = Transport::HttpAsync.guards();
let kafka = Transport::Kafka.guards();
let call = Transport::ChannelCall.guards();
for set in [sync, submit, kafka, call] {
assert!(set.rate_limit);
assert!(set.validation);
assert!(set.backpressure);
}
assert!(sync.origin_allow_list && submit.origin_allow_list);
assert!(!kafka.origin_allow_list && !call.origin_allow_list);
assert!(sync.deduplication && submit.deduplication && kafka.deduplication);
assert!(!call.deduplication);
assert!(sync.response_cache);
assert!(!submit.response_cache && !kafka.response_cache && !call.response_cache);
assert!(sync.auth && submit.auth);
assert!(!kafka.auth && !call.auth);
}
#[tokio::test]
async fn authentication_applies_to_every_http_ingress() {
let dl = engine();
let data = json!({});
let metadata = json!({});
for transport in [Transport::HttpSync, Transport::HttpAsync] {
let runtime = Runtime::new().api_key("s3cret").await.build();
let req = request(transport, &runtime, &dl, &data, &metadata);
assert!(
apply_guards(req).await.is_err(),
"{transport:?} admitted a request presenting no key"
);
let runtime = Runtime::new().api_key("s3cret").await.build();
let present: HeaderLookup<'_> =
&|name: &str| (name == "X-API-Key").then(|| "s3cret".to_string());
let mut req = request(transport, &runtime, &dl, &data, &metadata);
req.header = present;
assert!(
apply_guards(req).await.is_ok(),
"{transport:?} refused a request presenting the right key"
);
}
}
#[tokio::test]
async fn authentication_does_not_apply_to_kafka_or_channel_call() {
let dl = engine();
let data = json!({});
let metadata = json!({});
for transport in [Transport::Kafka, Transport::ChannelCall] {
let runtime = Runtime::new().api_key("s3cret").await.build();
let req = request(transport, &runtime, &dl, &data, &metadata);
assert!(
apply_guards(req).await.is_ok(),
"{transport:?} must not require an HTTP credential"
);
}
}
#[tokio::test]
async fn a_refused_caller_never_reaches_dedup_or_cache() {
let dl = engine();
let data = json!({});
let metadata = json!({});
let runtime = Runtime::new()
.api_key("s3cret")
.await
.dedup(
Arc::new(StubDedupBackend {
outcome: StubOutcome::BackendError,
}),
BackendErrorPolicy::Deny,
)
.build();
let mut req = request(Transport::HttpSync, &runtime, &dl, &data, &metadata);
req.header = IDEMPOTENCY;
let err = apply_guards(req)
.await
.err()
.expect("an unauthenticated caller must be refused");
assert!(
matches!(err, OrionError::Unauthorized(_)),
"expected a 401 from the auth guard, got {err:?} — the dedup guard ran first"
);
}
#[tokio::test]
async fn the_channel_timeout_outranks_the_transport_default() {
let with_timeout = Runtime::new().timeout_ms(2_000).build();
let without = Runtime::new().build();
assert_eq!(
effective_timeout_ms(&with_timeout, Some(60_000), None),
Some(2_000)
);
assert_eq!(effective_timeout_ms(&with_timeout, None, None), Some(2_000));
assert_eq!(
effective_timeout_ms(&without, Some(60_000), None),
Some(60_000)
);
assert_eq!(effective_timeout_ms(&without, None, None), None);
assert_eq!(
effective_timeout_ms(&None, Some(60_000), None),
Some(60_000)
);
}
#[tokio::test]
async fn a_transport_ceiling_clamps_an_over_long_channel_timeout() {
let over = Runtime::new().timeout_ms(600_000).build();
let under = Runtime::new().timeout_ms(2_000).build();
let none = Runtime::new().build();
assert_eq!(
effective_timeout_ms(&over, Some(30_000), Some(30_000)),
Some(30_000),
"a channel asking for 10 minutes gets the transport's ceiling"
);
assert_eq!(
effective_timeout_ms(&under, Some(30_000), Some(30_000)),
Some(2_000),
"a shorter channel deadline is still honoured"
);
assert_eq!(
effective_timeout_ms(&none, Some(30_000), Some(30_000)),
Some(30_000)
);
assert_eq!(
effective_timeout_ms(&over, None, None),
Some(600_000),
"without a ceiling the channel value is unchanged"
);
}
#[tokio::test]
async fn every_transport_carries_the_channel_timeout_into_its_admission() {
let dl = engine();
let runtime = Runtime::new().timeout_ms(2_000).build();
let (data, meta) = (json!({}), json!({}));
for transport in [
Transport::HttpSync,
Transport::HttpAsync,
Transport::Kafka,
Transport::ChannelCall,
] {
let mut req = request(transport, &runtime, &dl, &data, &meta);
req.default_timeout_ms = Some(60_000);
let admission = admitted(apply_guards(req).await.expect("guards pass"))
.expect("an admission, not a cache hit");
assert_eq!(
admission.timeout_ms,
Some(2_000),
"{transport:?} must honour the channel's timeout_ms"
);
}
}
#[tokio::test]
async fn the_rate_limit_applies_on_every_transport() {
let dl = engine();
let (data, meta) = (json!({}), json!({}));
for transport in [
Transport::HttpSync,
Transport::HttpAsync,
Transport::Kafka,
Transport::ChannelCall,
] {
let runtime = Runtime::new()
.limiter(
Arc::new(crate::channel::LocalRateLimitBackend::new(1, 1)),
BackendErrorPolicy::Allow,
)
.build();
let first = apply_guards(request(transport, &runtime, &dl, &data, &meta)).await;
assert!(first.is_ok(), "{transport:?} first call must pass");
let second = apply_guards(request(transport, &runtime, &dl, &data, &meta)).await;
assert!(
matches!(second, Err(OrionError::RateLimited(_))),
"{transport:?} must refuse the second call"
);
}
}
#[tokio::test]
async fn the_default_bucket_key_is_the_transports_own_caller_identity() {
let dl = engine();
let (data, meta) = (json!({}), json!({}));
let runtime = Runtime::new()
.limiter(
Arc::new(crate::channel::LocalRateLimitBackend::new(1, 1)),
BackendErrorPolicy::Allow,
)
.build();
let mut req = request(Transport::HttpSync, &runtime, &dl, &data, &meta);
req.caller_identity = "203.0.113.7";
assert!(apply_guards(req).await.is_ok());
let mut req = request(Transport::HttpSync, &runtime, &dl, &data, &meta);
req.caller_identity = "203.0.113.7";
assert!(matches!(
apply_guards(req).await,
Err(OrionError::RateLimited(_))
));
let mut req = request(Transport::Kafka, &runtime, &dl, &data, &meta);
req.caller_identity = "orders-topic";
assert!(
apply_guards(req).await.is_ok(),
"a Kafka record meters under its topic, not the HTTP client's bucket"
);
let shared = Runtime::new()
.limiter(
Arc::new(crate::channel::LocalRateLimitBackend::new(1, 1)),
BackendErrorPolicy::Allow,
)
.key_logic(&dl, json!({"var": "channel"}))
.build();
let mut req = request(Transport::HttpSync, &shared, &dl, &data, &meta);
req.caller_identity = "203.0.113.7";
assert!(apply_guards(req).await.is_ok());
let mut req = request(Transport::Kafka, &shared, &dl, &data, &meta);
req.caller_identity = "orders-topic";
assert!(
matches!(apply_guards(req).await, Err(OrionError::RateLimited(_))),
"a channel-keyed limit is spent by whichever ingress gets there first"
);
}
#[tokio::test]
async fn an_unevaluable_rate_limit_key_rejects_as_its_own_condition() {
let dl = engine();
let runtime = Runtime::new()
.limiter(
Arc::new(crate::channel::LocalRateLimitBackend::new(1000, 1000)),
BackendErrorPolicy::Allow,
)
.key_logic(&dl, json!({"throw": "key_unavailable"}))
.build();
let (data, meta) = (json!({}), json!({}));
let verdict = apply_guards(request(Transport::HttpSync, &runtime, &dl, &data, &meta)).await;
assert!(
matches!(verdict, Err(OrionError::RateLimitKeyUnavailable(_))),
"an unevaluable key must reject as unevaluable, not as over-limit"
);
let (status, code, _) = verdict.err().expect("a refusal").response_parts();
assert_eq!(status, axum::http::StatusCode::TOO_MANY_REQUESTS);
assert_eq!(code, "RATE_LIMITED");
}
#[tokio::test]
async fn a_limiter_backend_outage_follows_the_channel_policy() {
let dl = engine();
let (data, meta) = (json!({}), json!({}));
let allow = Runtime::new()
.limiter(Arc::new(FailingLimiter), BackendErrorPolicy::Allow)
.build();
let verdict = apply_guards(request(Transport::Kafka, &allow, &dl, &data, &meta)).await;
assert!(verdict.is_ok(), "allow must fail open");
let deny = Runtime::new()
.limiter(Arc::new(FailingLimiter), BackendErrorPolicy::Deny)
.build();
let verdict = apply_guards(request(Transport::Kafka, &deny, &dl, &data, &meta)).await;
assert!(
matches!(verdict, Err(OrionError::ServiceUnavailable(_))),
"deny must refuse with 503, not 429"
);
}
#[tokio::test]
async fn the_rate_limit_key_can_be_computed_from_transport_headers() {
let dl = engine();
let runtime = Runtime::new()
.limiter(
Arc::new(crate::channel::LocalRateLimitBackend::new(1, 1)),
BackendErrorPolicy::Allow,
)
.key_logic(&dl, json!({"var": "headers.x-tenant-id"}))
.build();
let (data, meta) = (json!({}), json!({}));
let acme = |name: &str| (name == "x-tenant-id").then(|| "acme".to_string());
let globex = |name: &str| (name == "x-tenant-id").then(|| "globex".to_string());
let mut req = request(Transport::Kafka, &runtime, &dl, &data, &meta);
req.header = &acme;
assert!(apply_guards(req).await.is_ok());
let mut req = request(Transport::Kafka, &runtime, &dl, &data, &meta);
req.header = &globex;
assert!(apply_guards(req).await.is_ok());
let mut req = request(Transport::Kafka, &runtime, &dl, &data, &meta);
req.header = &acme;
assert!(matches!(
apply_guards(req).await,
Err(OrionError::RateLimited(_))
));
}
#[tokio::test]
async fn backpressure_permits_are_shared_across_transports() {
let dl = engine();
let runtime = Runtime::new().backpressure(1).build();
let (data, meta) = (json!({}), json!({}));
let held = admitted(
apply_guards(request(Transport::HttpSync, &runtime, &dl, &data, &meta))
.await
.expect("first admission"),
)
.expect("an admission, not a cache hit");
assert!(held.backpressure_permit.is_some());
for transport in [Transport::Kafka, Transport::ChannelCall] {
let verdict = apply_guards(request(transport, &runtime, &dl, &data, &meta)).await;
assert!(
matches!(verdict, Err(OrionError::ServiceUnavailable(_))),
"{transport:?} must be shed while the permit is held"
);
}
drop(held);
assert!(
apply_guards(request(Transport::Kafka, &runtime, &dl, &data, &meta))
.await
.is_ok()
);
}
#[tokio::test]
async fn the_origin_allow_list_applies_to_http_only() {
let dl = engine();
let runtime = Runtime::new().origins(&["https://allowed.example"]).build();
let (data, meta) = (json!({}), json!({}));
for transport in [Transport::HttpSync, Transport::HttpAsync] {
let mut req = request(transport, &runtime, &dl, &data, &meta);
req.origin = Some("https://evil.example");
assert!(
matches!(apply_guards(req).await, Err(OrionError::Forbidden(_))),
"{transport:?} must refuse an unlisted origin"
);
}
for transport in [Transport::Kafka, Transport::ChannelCall] {
let mut req = request(transport, &runtime, &dl, &data, &meta);
req.origin = Some("https://evil.example");
assert!(
apply_guards(req).await.is_ok(),
"{transport:?} does not check origins"
);
}
}
#[tokio::test]
async fn the_pre_1_0_cors_spelling_cannot_produce_a_runtime() {
let stored = r#"{"cors": {"allowed_origins": ["https://allowed.example"]}}"#;
assert!(
serde_json::from_str::<crate::channel::ChannelConfig>(stored).is_err(),
"the old spelling must fail the config, not silently drop the allow-list"
);
let dl = engine();
let runtime = Runtime::new().build();
let (data, meta) = (json!({}), json!({}));
let mut req = request(Transport::HttpSync, &runtime, &dl, &data, &meta);
req.origin = Some("https://evil.example");
assert!(
apply_guards(req).await.is_ok(),
"a channel with no allow-list checks nothing — which is why the \
old spelling must not degrade into one"
);
}
#[tokio::test]
async fn validation_logic_applies_on_every_transport() {
let dl = engine();
let runtime = Runtime::new()
.validation(&dl, json!({"!!": {"var": "data.order_id"}}))
.build();
let bad = json!({});
let good = json!({"order_id": "ORD-1"});
let meta = json!({});
for transport in [
Transport::HttpSync,
Transport::HttpAsync,
Transport::Kafka,
Transport::ChannelCall,
] {
assert!(
matches!(
apply_guards(request(transport, &runtime, &dl, &bad, &meta)).await,
Err(OrionError::Validation { .. })
),
"{transport:?} must reject"
);
assert!(
apply_guards(request(transport, &runtime, &dl, &good, &meta))
.await
.is_ok(),
"{transport:?} must accept"
);
}
}
#[tokio::test]
async fn test_dedup_new_key_passes() {
let cfg = dedup_runtime(StubOutcome::New);
let result =
super::check_deduplication("test-channel", &cfg, IDEMPOTENCY, None, None).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_dedup_duplicate_rejected() {
let cfg = dedup_runtime(StubOutcome::Duplicate);
let result =
super::check_deduplication("test-channel", &cfg, IDEMPOTENCY, None, None).await;
assert!(matches!(result, Err(OrionError::Conflict(_))));
}
#[tokio::test]
async fn test_dedup_fails_open_on_backend_error_by_default() {
let cfg = dedup_runtime(StubOutcome::BackendError);
let result =
super::check_deduplication("test-channel", &cfg, IDEMPOTENCY, None, None).await;
assert!(result.is_ok(), "backend errors must fail open, not 409");
}
#[tokio::test]
async fn test_dedup_fails_closed_when_policy_is_deny() {
let cfg = dedup_runtime_with_policy(StubOutcome::BackendError, BackendErrorPolicy::Deny);
let result =
super::check_deduplication("test-channel", &cfg, IDEMPOTENCY, None, None).await;
assert!(
matches!(result, Err(OrionError::ServiceUnavailable(_))),
"deny must refuse with 503"
);
}
#[tokio::test]
async fn test_deny_policy_does_not_affect_healthy_backend() {
let cfg = dedup_runtime_with_policy(StubOutcome::New, BackendErrorPolicy::Deny);
let result =
super::check_deduplication("test-channel", &cfg, IDEMPOTENCY, None, None).await;
assert!(result.is_ok());
let cfg = dedup_runtime_with_policy(StubOutcome::Duplicate, BackendErrorPolicy::Deny);
let result =
super::check_deduplication("test-channel", &cfg, IDEMPOTENCY, None, None).await;
assert!(matches!(result, Err(OrionError::Conflict(_))));
}
#[tokio::test]
async fn test_dedup_key_is_channel_scoped() {
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
let cfg = Runtime::new()
.dedup(
Arc::new(CapturingDedupBackend { seen: seen.clone() }),
BackendErrorPolicy::Allow,
)
.build();
super::check_deduplication("orders", &cfg, IDEMPOTENCY, None, None)
.await
.expect("dedup check should pass");
let keys = seen.lock().expect("test lock poisoned");
assert_eq!(keys.as_slice(), ["dedup:orders:token-1"]);
}
#[tokio::test]
async fn the_kafka_record_key_is_the_fallback_idempotency_key() {
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
let cfg = Runtime::new()
.dedup(
Arc::new(CapturingDedupBackend { seen: seen.clone() }),
BackendErrorPolicy::Allow,
)
.build();
super::check_deduplication("orders", &cfg, NO_HEADERS, Some("ORD-77"), None)
.await
.expect("dedup check should pass");
assert_eq!(
seen.lock().expect("test lock poisoned").as_slice(),
["dedup:orders:ORD-77"]
);
let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
let cfg = Runtime::new()
.dedup(
Arc::new(CapturingDedupBackend { seen: seen.clone() }),
BackendErrorPolicy::Allow,
)
.build();
super::check_deduplication("orders", &cfg, IDEMPOTENCY, Some("ORD-77"), None)
.await
.expect("dedup check should pass");
assert_eq!(
seen.lock().expect("test lock poisoned").as_slice(),
["dedup:orders:token-1"]
);
}
#[tokio::test]
async fn channel_call_is_not_deduplicated() {
let dl = engine();
let runtime = Runtime::new()
.dedup(
Arc::new(StubDedupBackend {
outcome: StubOutcome::Duplicate,
}),
BackendErrorPolicy::Allow,
)
.build();
let (data, meta) = (json!({}), json!({}));
let mut req = request(Transport::ChannelCall, &runtime, &dl, &data, &meta);
req.header = IDEMPOTENCY;
assert!(
apply_guards(req).await.is_ok(),
"channel_call must not consult the dedup store"
);
for transport in [Transport::HttpSync, Transport::HttpAsync, Transport::Kafka] {
let mut req = request(transport, &runtime, &dl, &data, &meta);
req.header = IDEMPOTENCY;
assert!(
matches!(apply_guards(req).await, Err(OrionError::Conflict(_))),
"{transport:?} must reject a duplicate"
);
}
}
#[tokio::test]
async fn a_kafka_redelivery_recognises_its_own_unsettled_claim() {
let dl = engine();
let store = Arc::new(InMemoryDedupBackend::default());
let runtime = Runtime::new()
.dedup(store.clone(), BackendErrorPolicy::Allow)
.build();
let (data, meta) = (json!({}), json!({}));
let mut req = request(Transport::Kafka, &runtime, &dl, &data, &meta);
req.dedup_key_fallback = Some("ORD-77");
req.dedup_owner = Some("kafka:orders/0/7");
let admission = admitted(apply_guards(req).await.expect("first attempt admitted"))
.expect("an admission");
admission
.dedup_claim
.expect("a claim was taken")
.release()
.await;
let mut req = request(Transport::Kafka, &runtime, &dl, &data, &meta);
req.dedup_key_fallback = Some("ORD-77");
req.dedup_owner = Some("kafka:orders/0/7");
assert!(
apply_guards(req).await.is_ok(),
"a redelivery of an uncommitted offset must be processed, not committed as a duplicate"
);
}
#[tokio::test]
async fn a_claim_left_behind_by_a_dead_attempt_does_not_suppress_the_record() {
let dl = engine();
let store = Arc::new(InMemoryDedupBackend::default());
let runtime = Runtime::new()
.dedup(store.clone(), BackendErrorPolicy::Allow)
.build();
let (data, meta) = (json!({}), json!({}));
for _ in 0..3 {
let mut req = request(Transport::Kafka, &runtime, &dl, &data, &meta);
req.dedup_key_fallback = Some("ORD-77");
req.dedup_owner = Some("kafka:orders/0/7");
let admission = admitted(apply_guards(req).await.expect("admitted"))
.expect("an admission, not a cache hit");
drop(admission);
}
}
#[tokio::test]
async fn a_confirmed_claim_suppresses_every_later_delivery() {
let dl = engine();
let store = Arc::new(InMemoryDedupBackend::default());
let runtime = Runtime::new()
.dedup(store.clone(), BackendErrorPolicy::Allow)
.build();
let (data, meta) = (json!({}), json!({}));
let mut req = request(Transport::Kafka, &runtime, &dl, &data, &meta);
req.dedup_key_fallback = Some("ORD-77");
req.dedup_owner = Some("kafka:orders/0/7");
let admission = admitted(apply_guards(req).await.expect("admitted")).expect("an admission");
admission
.dedup_claim
.expect("a claim was taken")
.confirm()
.await;
let mut req = request(Transport::Kafka, &runtime, &dl, &data, &meta);
req.dedup_key_fallback = Some("ORD-77");
req.dedup_owner = Some("kafka:orders/0/12");
assert!(
matches!(apply_guards(req).await, Err(OrionError::Conflict(_))),
"a settled key must refuse a later record carrying it"
);
let mut req = request(Transport::Kafka, &runtime, &dl, &data, &meta);
req.dedup_key_fallback = Some("ORD-77");
req.dedup_owner = Some("kafka:orders/0/7");
assert!(
matches!(apply_guards(req).await, Err(OrionError::Conflict(_))),
"a settled key must refuse a replay of the record that settled it"
);
}
#[tokio::test]
async fn two_http_requests_with_one_key_are_still_a_duplicate() {
let dl = engine();
let store = Arc::new(InMemoryDedupBackend::default());
let runtime = Runtime::new()
.dedup(store.clone(), BackendErrorPolicy::Allow)
.build();
let (data, meta) = (json!({}), json!({}));
let mut req = request(Transport::HttpSync, &runtime, &dl, &data, &meta);
req.header = IDEMPOTENCY;
assert!(apply_guards(req).await.is_ok(), "the first request passes");
let mut req = request(Transport::HttpSync, &runtime, &dl, &data, &meta);
req.header = IDEMPOTENCY;
assert!(
matches!(apply_guards(req).await, Err(OrionError::Conflict(_))),
"the second must be refused"
);
}
#[tokio::test]
async fn a_shed_request_releases_the_key_it_claimed() {
let dl = engine();
let store = Arc::new(InMemoryDedupBackend::default());
let runtime = Runtime::new()
.dedup(store.clone(), BackendErrorPolicy::Allow)
.backpressure(1)
.build();
let (data, meta) = (json!({}), json!({}));
let held = admitted(
apply_guards(request(Transport::HttpSync, &runtime, &dl, &data, &meta))
.await
.expect("first admission"),
)
.expect("an admission");
let mut req = request(Transport::HttpSync, &runtime, &dl, &data, &meta);
req.header = IDEMPOTENCY;
assert!(matches!(
apply_guards(req).await,
Err(OrionError::ServiceUnavailable(_))
));
assert!(
store
.get("dedup:orders:token-1")
.await
.expect("store readable")
.is_none(),
"a shed request must leave no claim behind"
);
drop(held);
let mut req = request(Transport::HttpSync, &runtime, &dl, &data, &meta);
req.header = IDEMPOTENCY;
assert!(
apply_guards(req).await.is_ok(),
"the retry of a shed request must not be refused as a duplicate"
);
}
#[tokio::test]
async fn only_sync_http_reads_the_response_cache() {
let dl = engine();
let runtime = Runtime::new().cache(Arc::new(AlwaysHitCache)).build();
let (data, meta) = (json!({}), json!({}));
let verdict = apply_guards(request(Transport::HttpSync, &runtime, &dl, &data, &meta))
.await
.expect("guards pass");
assert!(matches!(verdict, GuardVerdict::CacheHit(ref body) if body.contains("cached")));
for transport in [
Transport::HttpAsync,
Transport::Kafka,
Transport::ChannelCall,
] {
let verdict = apply_guards(request(transport, &runtime, &dl, &data, &meta))
.await
.expect("guards pass");
let admission = admitted(verdict).expect("an admission, not a cache hit");
assert!(
admission.cache_store.is_none(),
"{transport:?} must neither read nor write the response cache"
);
}
}
#[tokio::test]
async fn dedup_precedes_the_cache_lookup() {
let dl = engine();
let runtime = Runtime::new()
.dedup(
Arc::new(StubDedupBackend {
outcome: StubOutcome::Duplicate,
}),
BackendErrorPolicy::Allow,
)
.cache(Arc::new(AlwaysHitCache))
.build();
let (data, meta) = (json!({}), json!({}));
let mut req = request(Transport::HttpSync, &runtime, &dl, &data, &meta);
req.header = IDEMPOTENCY;
assert!(matches!(
apply_guards(req).await,
Err(OrionError::Conflict(_))
));
}
#[tokio::test]
async fn a_cache_hit_takes_no_backpressure_permit() {
let dl = engine();
let runtime = Runtime::new()
.cache(Arc::new(AlwaysHitCache))
.backpressure(1)
.build();
let (data, meta) = (json!({}), json!({}));
for _ in 0..3 {
let verdict = apply_guards(request(Transport::HttpSync, &runtime, &dl, &data, &meta))
.await
.expect("guards pass");
assert!(matches!(verdict, GuardVerdict::CacheHit(_)));
}
let semaphore = runtime
.as_ref()
.expect("runtime")
.backpressure_semaphore
.as_ref()
.expect("semaphore");
assert_eq!(semaphore.available_permits(), 1);
}
fn cache_cfg(fields: Option<Vec<String>>) -> crate::channel::ChannelCacheConfig {
crate::channel::ChannelCacheConfig {
enabled: true,
ttl_secs: Some(60),
cache_key_fields: fields,
connector: None,
}
}
fn key(
channel: &str,
data: &serde_json::Value,
metadata: &serde_json::Value,
cfg: &crate::channel::ChannelCacheConfig,
) -> String {
super::compute_cache_key(channel, data, metadata, cfg)
.expect("this request must have a cache key")
}
fn meta(
method: &str,
params: serde_json::Value,
query: serde_json::Value,
) -> serde_json::Value {
serde_json::json!({
"http_method": method,
"params": params,
"query": query,
"headers": {},
})
}
#[test]
fn test_cache_key_distinguishes_route_params() {
let data = serde_json::json!({});
let a = key(
"orders",
&data,
&meta("GET", serde_json::json!({"id": "1"}), serde_json::json!({})),
&cache_cfg(None),
);
let b = key(
"orders",
&data,
&meta("GET", serde_json::json!({"id": "2"}), serde_json::json!({})),
&cache_cfg(None),
);
assert_ne!(a, b, "different path params must not share a cache entry");
}
#[test]
fn test_cache_key_distinguishes_query_and_method() {
let data = serde_json::json!({});
let base = meta(
"GET",
serde_json::json!({}),
serde_json::json!({"page": "1"}),
);
let a = key("orders", &data, &base, &cache_cfg(None));
let b = key(
"orders",
&data,
&meta(
"GET",
serde_json::json!({}),
serde_json::json!({"page": "2"}),
),
&cache_cfg(None),
);
let c = key(
"orders",
&data,
&meta(
"POST",
serde_json::json!({}),
serde_json::json!({"page": "1"}),
),
&cache_cfg(None),
);
assert_ne!(a, b);
assert_ne!(a, c);
}
#[test]
fn test_cache_key_stable_for_identical_requests() {
let data = serde_json::json!({"order_id": 7});
let m = meta(
"GET",
serde_json::json!({"id": "1"}),
serde_json::json!({"expand": "items"}),
);
let a = key("orders", &data, &m, &cache_cfg(None));
let b = key("orders", &data, &m, &cache_cfg(None));
assert_eq!(a, b);
}
#[test]
fn documented_data_prefixed_paths_distinguish_callers() {
let m = meta("POST", serde_json::json!({}), serde_json::json!({}));
let fields = Some(vec!["data.user_id".to_string(), "data.action".to_string()]);
let alice = serde_json::json!({"user_id": "alice", "action": "balance"});
let bob = serde_json::json!({"user_id": "bob", "action": "balance"});
assert_ne!(
key("acct", &alice, &m, &cache_cfg(fields.clone())),
key("acct", &bob, &m, &cache_cfg(fields)),
"distinct users must not share a cache entry"
);
}
#[test]
fn dotted_paths_walk_into_nested_objects() {
let m = meta("POST", serde_json::json!({}), serde_json::json!({}));
let fields = Some(vec!["user.id".to_string()]);
let a = serde_json::json!({"user": {"id": 1}});
let b = serde_json::json!({"user": {"id": 2}});
assert_ne!(
key("c", &a, &m, &cache_cfg(fields.clone())),
key("c", &b, &m, &cache_cfg(fields))
);
}
#[test]
fn a_literal_dotted_payload_key_still_wins() {
let m = meta("POST", serde_json::json!({}), serde_json::json!({}));
let fields = Some(vec!["a.b".to_string()]);
let flat_1 = serde_json::json!({"a.b": 1});
let flat_2 = serde_json::json!({"a.b": 2});
assert_ne!(
key("c", &flat_1, &m, &cache_cfg(fields.clone())),
key("c", &flat_2, &m, &cache_cfg(fields.clone()))
);
let nested = serde_json::json!({"a": {"b": 1}});
assert_eq!(
key("c", &flat_1, &m, &cache_cfg(fields.clone())),
key("c", &nested, &m, &cache_cfg(fields))
);
}
#[test]
fn the_key_is_stable_across_processes() {
let m = meta(
"POST",
serde_json::json!({"id": "7"}),
serde_json::json!({"expand": "items"}),
);
let data = serde_json::json!({"order_id": 7, "nested": {"a": [1, 2, 3]}});
assert_eq!(
key("orders", &data, &m, &cache_cfg(None)),
"cache:orders:47396736ec3c2fde9455d2f9a9161e91"
);
}
#[test]
fn key_ignores_map_ordering() {
let data = serde_json::json!({});
let a = meta(
"GET",
serde_json::json!({}),
serde_json::json!({"a": "1", "b": "2"}),
);
let b = meta(
"GET",
serde_json::json!({}),
serde_json::json!({"b": "2", "a": "1"}),
);
assert_eq!(
key("c", &data, &a, &cache_cfg(None)),
key("c", &data, &b, &cache_cfg(None))
);
}
#[test]
fn framing_separates_adjacent_chunks() {
let data = serde_json::json!({});
let a = meta("GET", serde_json::json!({"ab": "c"}), serde_json::json!({}));
let b = meta("GET", serde_json::json!({"a": "bc"}), serde_json::json!({}));
assert_ne!(
key("c", &data, &a, &cache_cfg(None)),
key("c", &data, &b, &cache_cfg(None))
);
}
#[test]
fn a_payload_matching_no_declared_field_has_no_key() {
let m = meta("POST", serde_json::json!({}), serde_json::json!({}));
let fields = Some(vec!["user_id".to_string(), "action".to_string()]);
assert!(
super::compute_cache_key(
"acct",
&serde_json::json!({"unrelated": 1}),
&m,
&cache_cfg(fields)
)
.is_none()
);
}
#[test]
fn absent_fields_are_not_silently_skipped() {
let m = meta("POST", serde_json::json!({}), serde_json::json!({}));
let fields = Some(vec!["a".to_string(), "b".to_string()]);
assert_ne!(
key(
"c",
&serde_json::json!({"a": 1}),
&m,
&cache_cfg(fields.clone())
),
key("c", &serde_json::json!({"b": 1}), &m, &cache_cfg(fields))
);
}
#[test]
fn test_cache_key_folds_route_identity_with_key_fields() {
let data = serde_json::json!({"tenant": "acme"});
let fields = Some(vec!["tenant".to_string()]);
let a = key(
"orders",
&data,
&meta("GET", serde_json::json!({"id": "1"}), serde_json::json!({})),
&cache_cfg(fields.clone()),
);
let b = key(
"orders",
&data,
&meta("GET", serde_json::json!({"id": "2"}), serde_json::json!({})),
&cache_cfg(fields),
);
assert_ne!(a, b);
}
}