use super::*;
use crate::agent_executor;
use crate::builder::RequestHandlerBuilder;
use crate::tenant_config::{PerTenantConfig, TenantLimits};
use crate::tenant_resolver::HeaderTenantResolver;
struct DummyExecutor;
agent_executor!(DummyExecutor, |_ctx, _queue| async { Ok(()) });
#[test]
fn default_build_has_no_tenant_resolver() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.build()
.expect("default build should succeed");
assert!(
handler.tenant_resolver().is_none(),
"default handler should have no tenant resolver"
);
}
#[test]
fn tenant_overrides_resolve_ahead_of_the_defaults() {
use std::time::Duration;
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_tenant_config(
PerTenantConfig::builder()
.default_limits(TenantLimits::builder().max_concurrent_tasks(1).build())
.with_override(
"premium",
TenantLimits::builder()
.executor_timeout(Duration::from_secs(999))
.max_concurrent_tasks(64)
.build(),
)
.build(),
)
.build()
.expect("build");
let config = handler.tenant_config().expect("tenant config");
let premium = config.get("premium");
assert_eq!(premium.executor_timeout, Some(Duration::from_secs(999)));
assert_eq!(premium.max_concurrent_tasks, Some(64));
assert_eq!(config.get("someone-else").max_concurrent_tasks, Some(1));
assert_eq!(config.get("someone-else").executor_timeout, None);
}
#[test]
fn default_build_has_no_tenant_config() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.build()
.expect("default build should succeed");
assert!(
handler.tenant_config().is_none(),
"default handler should have no tenant config"
);
}
#[test]
fn tenant_resolver_returns_some_when_configured() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_tenant_resolver(HeaderTenantResolver::default())
.build()
.expect("build with tenant resolver");
assert!(
handler.tenant_resolver().is_some(),
"should return Some when a resolver was configured"
);
}
fn headers_with(tenant: &str) -> HashMap<String, String> {
let mut h = HashMap::new();
h.insert("x-tenant-id".to_owned(), tenant.to_owned());
h
}
#[tokio::test]
async fn resolve_tenant_uses_resolver_when_client_omits_tenant() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_tenant_resolver(HeaderTenantResolver::default())
.build()
.unwrap();
let headers = headers_with("acme");
let tenant = handler
.resolve_tenant("GetTask", Some(&headers), None)
.await
.expect("resolution should succeed");
assert_eq!(tenant, "acme", "resolver-derived tenant must be used");
}
#[tokio::test]
async fn resolve_tenant_rejects_client_tenant_mismatch() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_tenant_resolver(HeaderTenantResolver::default())
.build()
.unwrap();
let headers = headers_with("acme");
let result = handler
.resolve_tenant("GetTask", Some(&headers), Some("victim"))
.await;
assert!(
matches!(result, Err(crate::error::ServerError::InvalidParams(_))),
"a client tenant disagreeing with the resolver must be rejected, got {result:?}"
);
}
#[tokio::test]
async fn resolve_tenant_accepts_matching_client_tenant() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_tenant_resolver(HeaderTenantResolver::default())
.build()
.unwrap();
let headers = headers_with("acme");
let tenant = handler
.resolve_tenant("GetTask", Some(&headers), Some("acme"))
.await
.expect("matching client tenant is fine");
assert_eq!(tenant, "acme");
}
#[tokio::test]
async fn resolve_tenant_default_falls_back_to_empty_when_unresolved() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_tenant_resolver(HeaderTenantResolver::default())
.build()
.unwrap();
let tenant = handler
.resolve_tenant("GetTask", None, None)
.await
.expect("default mode tolerates an unresolved tenant");
assert_eq!(
tenant, "",
"unresolved tenant defaults to the shared partition"
);
}
#[tokio::test]
async fn resolve_tenant_strict_rejects_unresolved() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_tenant_resolver(HeaderTenantResolver::default())
.require_resolved_tenant()
.build()
.unwrap();
let result = handler.resolve_tenant("GetTask", None, None).await;
assert!(
matches!(result, Err(crate::error::ServerError::InvalidParams(_))),
"strict mode must reject an unresolved tenant, got {result:?}"
);
}
#[tokio::test]
async fn resolve_tenant_without_resolver_trusts_client_value() {
let handler = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
let tenant = handler
.resolve_tenant("GetTask", None, Some("whatever"))
.await
.unwrap();
assert_eq!(tenant, "whatever");
}
#[test]
fn tenant_resolver_returns_none_when_not_configured() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.build()
.expect("default build");
assert!(
handler.tenant_resolver().is_none(),
"should return None when no resolver was configured"
);
}
#[test]
fn tenant_config_returns_some_when_configured() {
let config = PerTenantConfig::builder()
.default_limits(TenantLimits::builder().rate_limit_rps(50).build())
.build();
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_tenant_config(config)
.build()
.expect("build with tenant config");
assert!(
handler.tenant_config().is_some(),
"should return Some when tenant config was provided"
);
}
#[test]
fn tenant_config_returns_none_when_not_configured() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.build()
.expect("default build");
assert!(
handler.tenant_config().is_none(),
"should return None when no tenant config was provided"
);
}
#[test]
fn tenant_config_preserves_values() {
let config = PerTenantConfig::builder()
.default_limits(TenantLimits::builder().rate_limit_rps(100).build())
.with_override("vip", TenantLimits::builder().rate_limit_rps(500).build())
.build();
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_tenant_config(config)
.build()
.expect("build with per-tenant overrides");
let cfg = handler.tenant_config().expect("config should be Some");
assert_eq!(cfg.get("vip").rate_limit_rps, Some(500));
assert_eq!(cfg.get("unknown-tenant").rate_limit_rps, Some(100));
}
#[test]
fn handler_with_both_tenant_fields() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.with_tenant_resolver(HeaderTenantResolver::default())
.with_tenant_config(
PerTenantConfig::builder()
.default_limits(TenantLimits::builder().rate_limit_rps(10).build())
.build(),
)
.build()
.expect("build with both tenant resolver and config");
assert!(handler.tenant_resolver().is_some());
assert!(handler.tenant_config().is_some());
}
#[test]
fn debug_impl_does_not_panic() {
let handler = RequestHandlerBuilder::new(DummyExecutor)
.build()
.expect("default build");
let debug = format!("{handler:?}");
assert!(
debug.contains("RequestHandler"),
"Debug output should contain struct name"
);
}
#[test]
fn debug_shows_tenant_resolver_presence() {
let without = RequestHandlerBuilder::new(DummyExecutor).build().unwrap();
let with = RequestHandlerBuilder::new(DummyExecutor)
.with_tenant_resolver(HeaderTenantResolver::default())
.build()
.unwrap();
let dbg_without = format!("{without:?}");
let dbg_with = format!("{with:?}");
assert!(
dbg_without.contains("tenant_resolver: false"),
"should show false when no resolver: {dbg_without}"
);
assert!(
dbg_with.contains("tenant_resolver: true"),
"should show true when resolver configured: {dbg_with}"
);
}
#[test]
fn send_message_result_response_variant() {
use a2a_protocol_types::responses::SendMessageResponse;
use a2a_protocol_types::task::{Task, TaskState, TaskStatus};
let task = Task {
id: "t1".into(),
context_id: "c1".into(),
status: TaskStatus::new(TaskState::Completed),
artifacts: None,
history: None,
metadata: None,
};
let result = SendMessageResult::Response(SendMessageResponse::Task(task));
assert!(matches!(result, SendMessageResult::Response(_)));
}