use scim_server::{IsolationLevel, RequestContext, TenantContext, TenantPermissions};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct TenantContextBuilder {
tenant_id: String,
client_id: String,
isolation_level: IsolationLevel,
permissions: Vec<String>,
}
impl TenantContextBuilder {
pub fn new(tenant_id: &str) -> Self {
Self {
tenant_id: tenant_id.to_string(),
client_id: format!("{}_client", tenant_id),
isolation_level: IsolationLevel::Standard,
permissions: vec!["read".to_string(), "write".to_string()],
}
}
pub fn with_client_id(mut self, client_id: &str) -> Self {
self.client_id = client_id.to_string();
self
}
pub fn with_isolation_level(mut self, level: IsolationLevel) -> Self {
self.isolation_level = level;
self
}
pub fn with_permissions(mut self, permissions: Vec<&str>) -> Self {
self.permissions = permissions.into_iter().map(String::from).collect();
self
}
pub fn build(self) -> TenantContext {
let mut permissions = TenantPermissions::default();
permissions.can_create = self.permissions.contains(&"create".to_string())
|| self.permissions.contains(&"write".to_string());
permissions.can_read = self.permissions.contains(&"read".to_string());
permissions.can_update = self.permissions.contains(&"update".to_string())
|| self.permissions.contains(&"write".to_string());
permissions.can_delete = self.permissions.contains(&"delete".to_string())
|| self.permissions.contains(&"write".to_string());
permissions.can_list = self.permissions.contains(&"list".to_string())
|| self.permissions.contains(&"read".to_string());
TenantContext::new(self.tenant_id, self.client_id)
.with_isolation_level(self.isolation_level)
.with_permissions(permissions)
}
}
#[derive(Debug, Clone)]
pub struct AuthInfoBuilder {
api_key: Option<String>,
bearer_token: Option<String>,
client_certificate: Option<Vec<u8>>,
}
impl AuthInfoBuilder {
pub fn new() -> Self {
Self {
api_key: None,
bearer_token: None,
client_certificate: None,
}
}
pub fn with_api_key(mut self, key: &str) -> Self {
self.api_key = Some(key.to_string());
self
}
pub fn with_bearer_token(mut self, token: &str) -> Self {
self.bearer_token = Some(token.to_string());
self
}
pub fn build(self) -> AuthInfo {
AuthInfo {
api_key: self.api_key,
bearer_token: self.bearer_token,
client_certificate: self.client_certificate,
}
}
}
#[derive(Debug, Clone)]
pub struct AuthInfo {
pub api_key: Option<String>,
pub bearer_token: Option<String>,
pub client_certificate: Option<Vec<u8>>,
}
pub trait TenantResolver: Send + Sync {
type Error: std::error::Error + Send + Sync + 'static;
fn resolve_tenant(&self, auth_info: &AuthInfo) -> Result<TenantContext, Self::Error>;
}
pub struct TestTenantResolver {
tenant_mappings: HashMap<String, TenantContext>,
}
impl TestTenantResolver {
pub fn new() -> Self {
let mut mappings = HashMap::new();
mappings.insert(
"api_key_tenant_a".to_string(),
TenantContextBuilder::new("tenant_a")
.with_client_id("client_a")
.build(),
);
mappings.insert(
"api_key_tenant_b".to_string(),
TenantContextBuilder::new("tenant_b")
.with_client_id("client_b")
.build(),
);
Self {
tenant_mappings: mappings,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum TenantError {
#[error("Invalid authentication credentials")]
InvalidCredentials,
#[error("Tenant not found for credentials")]
TenantNotFound,
#[error("Unauthorized access to tenant {tenant_id}")]
UnauthorizedAccess { tenant_id: String },
}
impl TenantResolver for TestTenantResolver {
type Error = TenantError;
fn resolve_tenant(&self, auth_info: &AuthInfo) -> Result<TenantContext, Self::Error> {
if let Some(api_key) = &auth_info.api_key {
self.tenant_mappings
.get(api_key)
.cloned()
.ok_or(TenantError::TenantNotFound)
} else {
Err(TenantError::InvalidCredentials)
}
}
}
#[cfg(test)]
mod core_multi_tenant_tests {
use super::*;
#[test]
fn test_tenant_context_creation() {
let tenant_context = TenantContextBuilder::new("tenant_123")
.with_client_id("client_abc")
.with_isolation_level(IsolationLevel::Strict)
.with_permissions(vec!["read", "write", "delete"])
.build();
assert_eq!(tenant_context.tenant_id, "tenant_123");
assert_eq!(tenant_context.client_id, "client_abc");
assert_eq!(tenant_context.isolation_level, IsolationLevel::Strict);
assert!(tenant_context.permissions.can_read);
assert!(tenant_context.permissions.can_create);
assert!(tenant_context.permissions.can_update);
assert!(tenant_context.permissions.can_delete);
}
#[test]
fn test_request_context_with_tenant() {
let tenant_context = TenantContextBuilder::new("tenant_456").build();
let request_context =
scim_server::RequestContext::with_tenant("req_123".to_string(), tenant_context.clone());
assert_eq!(request_context.request_id, "req_123");
assert_eq!(request_context.tenant_id(), Some("tenant_456"));
}
#[test]
fn test_isolation_level_variants() {
let strict = IsolationLevel::Strict;
let standard = IsolationLevel::Standard;
let shared = IsolationLevel::Shared;
assert_ne!(strict, standard);
assert_ne!(standard, shared);
assert_ne!(strict, shared);
}
#[test]
fn test_tenant_resolver_successful_resolution() {
let resolver = TestTenantResolver::new();
let auth_info = AuthInfoBuilder::new()
.with_api_key("api_key_tenant_a")
.build();
let result = resolver.resolve_tenant(&auth_info);
assert!(result.is_ok());
let tenant_context = result.unwrap();
assert_eq!(tenant_context.tenant_id, "tenant_a");
assert_eq!(tenant_context.client_id, "client_a");
}
#[test]
fn test_tenant_resolver_invalid_api_key() {
let resolver = TestTenantResolver::new();
let auth_info = AuthInfoBuilder::new()
.with_api_key("invalid_api_key")
.build();
let result = resolver.resolve_tenant(&auth_info);
assert!(result.is_err());
match result.unwrap_err() {
TenantError::TenantNotFound => {}
other => panic!("Expected TenantNotFound, got {:?}", other),
}
}
#[test]
fn test_tenant_resolver_missing_credentials() {
let resolver = TestTenantResolver::new();
let auth_info = AuthInfoBuilder::new().build();
let result = resolver.resolve_tenant(&auth_info);
assert!(result.is_err());
match result.unwrap_err() {
TenantError::InvalidCredentials => {}
other => panic!("Expected InvalidCredentials, got {:?}", other),
}
}
#[test]
fn test_tenant_resolver_different_tenants() {
let resolver = TestTenantResolver::new();
let auth_a = AuthInfoBuilder::new()
.with_api_key("api_key_tenant_a")
.build();
let auth_b = AuthInfoBuilder::new()
.with_api_key("api_key_tenant_b")
.build();
let tenant_a = resolver.resolve_tenant(&auth_a).unwrap();
let tenant_b = resolver.resolve_tenant(&auth_b).unwrap();
assert_eq!(tenant_a.tenant_id, "tenant_a");
assert_eq!(tenant_b.tenant_id, "tenant_b");
assert_ne!(tenant_a.tenant_id, tenant_b.tenant_id);
assert_ne!(tenant_a.client_id, tenant_b.client_id);
}
#[test]
fn test_tenant_error_types() {
let invalid_creds = TenantError::InvalidCredentials;
let tenant_not_found = TenantError::TenantNotFound;
let unauthorized = TenantError::UnauthorizedAccess {
tenant_id: "tenant_123".to_string(),
};
assert!(invalid_creds.to_string().contains("Invalid authentication"));
assert!(tenant_not_found.to_string().contains("Tenant not found"));
assert!(
unauthorized
.to_string()
.contains("Unauthorized access to tenant tenant_123")
);
}
#[test]
fn test_tenant_contexts_are_isolated() {
let tenant_a = TenantContextBuilder::new("tenant_a").build();
let tenant_b = TenantContextBuilder::new("tenant_b").build();
assert_ne!(tenant_a.tenant_id, tenant_b.tenant_id);
assert_ne!(tenant_a.client_id, tenant_b.client_id);
assert_ne!(tenant_a.tenant_id, tenant_b.tenant_id);
}
#[test]
fn test_tenant_context_immutability() {
let tenant_context = TenantContextBuilder::new("tenant_123")
.with_client_id("client_abc")
.build();
let cloned_context = tenant_context.clone();
assert_eq!(tenant_context.tenant_id, cloned_context.tenant_id);
assert_eq!(tenant_context.client_id, cloned_context.client_id);
}
#[test]
fn test_migration_from_current_request_context() {
let single_tenant_context = RequestContext::new("req_123".to_string());
assert_eq!(single_tenant_context.request_id, "req_123");
assert!(!single_tenant_context.is_multi_tenant());
assert_eq!(single_tenant_context.tenant_id(), None);
let tenant_context = TenantContextBuilder::new("tenant_456").build();
let multi_tenant_context =
scim_server::RequestContext::with_tenant("req_456".to_string(), tenant_context);
assert_eq!(multi_tenant_context.request_id, "req_456");
assert!(multi_tenant_context.is_multi_tenant());
assert_eq!(multi_tenant_context.tenant_id(), Some("tenant_456"));
}
#[test]
fn test_tenant_permission_validation() {
let read_only_tenant = TenantContextBuilder::new("readonly_tenant")
.with_permissions(vec!["read"])
.build();
let full_access_tenant = TenantContextBuilder::new("full_access_tenant")
.with_permissions(vec!["read", "write", "delete"])
.build();
assert!(read_only_tenant.permissions.can_read);
assert!(!read_only_tenant.permissions.can_create);
assert!(!read_only_tenant.permissions.can_update);
assert!(!read_only_tenant.permissions.can_delete);
assert!(full_access_tenant.permissions.can_read);
assert!(full_access_tenant.permissions.can_create);
assert!(full_access_tenant.permissions.can_update);
assert!(full_access_tenant.permissions.can_delete);
}
#[test]
fn test_strict_isolation_requirements() {
let strict_tenant = TenantContextBuilder::new("strict_tenant")
.with_isolation_level(IsolationLevel::Strict)
.build();
assert_eq!(strict_tenant.isolation_level, IsolationLevel::Strict);
println!("Strict isolation tenant: {}", strict_tenant.tenant_id);
println!("Expected behavior: Complete database/schema separation");
}
#[test]
fn test_multi_tenant_usage_examples() {
println!("\n🏢 Multi-Tenant Usage Examples");
println!("===============================");
let org_a = TenantContextBuilder::new("org_acme")
.with_client_id("acme_client")
.with_isolation_level(IsolationLevel::Standard)
.build();
let org_b = TenantContextBuilder::new("org_beta")
.with_client_id("beta_client")
.with_isolation_level(IsolationLevel::Strict)
.build();
println!(
"Organization A: {} (isolation: {:?})",
org_a.tenant_id, org_a.isolation_level
);
println!(
"Organization B: {} (isolation: {:?})",
org_b.tenant_id, org_b.isolation_level
);
let admin_tenant = TenantContextBuilder::new("admin_tenant")
.with_permissions(vec!["read", "write", "delete", "admin"])
.build();
let viewer_tenant = TenantContextBuilder::new("viewer_tenant")
.with_permissions(vec!["read"])
.build();
println!("Admin tenant permissions: {:?}", admin_tenant.permissions);
println!("Viewer tenant permissions: {:?}", viewer_tenant.permissions);
assert_ne!(org_a.tenant_id, org_b.tenant_id);
assert_ne!(admin_tenant.tenant_id, viewer_tenant.tenant_id);
}
}
pub struct TenantFixtures;
impl TenantFixtures {
pub fn standard_saas_tenant(tenant_id: &str) -> TenantContext {
TenantContextBuilder::new(tenant_id)
.with_isolation_level(IsolationLevel::Standard)
.with_permissions(vec!["read", "write"])
.build()
}
pub fn high_security_tenant(tenant_id: &str) -> TenantContext {
TenantContextBuilder::new(tenant_id)
.with_isolation_level(IsolationLevel::Strict)
.with_permissions(vec!["read", "write", "delete"])
.build()
}
pub fn read_only_tenant(tenant_id: &str) -> TenantContext {
TenantContextBuilder::new(tenant_id)
.with_permissions(vec!["read"])
.build()
}
}
pub struct MultiTenantTestContext {
pub resolver: TestTenantResolver,
pub tenant_a: TenantContext,
pub tenant_b: TenantContext,
}
impl MultiTenantTestContext {
pub fn new() -> Self {
Self {
resolver: TestTenantResolver::new(),
tenant_a: TenantFixtures::standard_saas_tenant("tenant_a"),
tenant_b: TenantFixtures::standard_saas_tenant("tenant_b"),
}
}
pub fn create_auth_for_tenant_a(&self) -> AuthInfo {
AuthInfoBuilder::new()
.with_api_key("api_key_tenant_a")
.build()
}
pub fn create_auth_for_tenant_b(&self) -> AuthInfo {
AuthInfoBuilder::new()
.with_api_key("api_key_tenant_b")
.build()
}
}