use crate::matchers::RequestContext;
use crate::tenant::Namespace;
pub trait TenantResolver: Send + Sync {
fn resolve(&self, ctx: &RequestContext) -> Option<Namespace>;
}
#[derive(Debug, Clone, Default)]
pub struct DefaultTenantResolver;
impl TenantResolver for DefaultTenantResolver {
fn resolve(&self, _ctx: &RequestContext) -> Option<Namespace> {
Some(Namespace::default())
}
}
#[derive(Debug, Clone)]
pub struct HeaderTenantResolver {
header_name: String,
environment: String,
}
impl HeaderTenantResolver {
pub fn new(header_name: impl Into<String>, environment: impl Into<String>) -> Self {
Self {
header_name: header_name.into(),
environment: environment.into(),
}
}
}
impl TenantResolver for HeaderTenantResolver {
fn resolve(&self, ctx: &RequestContext) -> Option<Namespace> {
ctx.get_header(&self.header_name)
.map(|tenant_id| Namespace::new(tenant_id, &self.environment))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_tenant_resolver() {
let resolver = DefaultTenantResolver;
let ctx = RequestContext::new();
let namespace = resolver.resolve(&ctx).unwrap();
assert_eq!(namespace.tenant_id(), "global");
assert_eq!(namespace.environment(), "development");
}
#[test]
fn test_header_tenant_resolver_success() {
let resolver = HeaderTenantResolver::new("X-Tenant-ID", "production");
let ctx = RequestContext::new().with_header("X-Tenant-ID", "acme-corp");
let namespace = resolver.resolve(&ctx).unwrap();
assert_eq!(namespace.tenant_id(), "acme-corp");
assert_eq!(namespace.environment(), "production");
}
#[test]
fn test_header_tenant_resolver_missing_header() {
let resolver = HeaderTenantResolver::new("X-Tenant-ID", "production");
let ctx = RequestContext::new();
let result = resolver.resolve(&ctx);
assert!(result.is_none());
}
#[test]
fn test_header_tenant_resolver_case_insensitive() {
let resolver = HeaderTenantResolver::new("x-tenant-id", "production");
let ctx = RequestContext::new().with_header("X-Tenant-ID", "acme-corp");
let namespace = resolver.resolve(&ctx).unwrap();
assert_eq!(namespace.tenant_id(), "acme-corp");
}
#[test]
fn test_default_produces_default_namespace() {
let resolver = DefaultTenantResolver;
let ctx = RequestContext::new();
let namespace = resolver.resolve(&ctx).unwrap();
assert_eq!(namespace, Namespace::default());
}
#[test]
fn test_header_resolver_with_different_environments() {
let resolver = HeaderTenantResolver::new("X-Env", "staging");
let ctx = RequestContext::new().with_header("X-Env", "customer-42");
let ns = resolver.resolve(&ctx).unwrap();
assert_eq!(ns.tenant_id(), "customer-42");
assert_eq!(ns.environment(), "staging");
}
#[test]
fn test_header_resolver_non_matching_header_name() {
let resolver = HeaderTenantResolver::new("Authorization", "production");
let ctx = RequestContext::new().with_header("X-Tenant-ID", "acme");
let result = resolver.resolve(&ctx);
assert!(
result.is_none(),
"Authorization header should not match X-Tenant-ID"
);
}
#[test]
fn test_header_resolver_empty_header_value() {
let resolver = HeaderTenantResolver::new("X-Tenant-ID", "production");
let ctx = RequestContext::new().with_header("X-Tenant-ID", "");
let ns = resolver.resolve(&ctx).unwrap();
assert_eq!(
ns.tenant_id(),
"",
"tenant_id should be empty string from empty header value"
);
}
#[test]
fn test_header_resolver_debug_output() {
let resolver = HeaderTenantResolver::new("X-Tenant-ID", "production");
let debug = format!("{:?}", resolver);
assert!(
debug.contains("HeaderTenantResolver"),
"Debug output should contain type name"
);
assert!(
debug.contains("X-Tenant-ID") || debug.contains("x-tenant-id"),
"Debug output should contain header name"
);
}
}