Skip to main content

heliosdb_proxy/multi_tenancy/
config.rs

1//! Multi-Tenancy Configuration Types
2//!
3//! This module provides configuration structures for multi-tenant proxy operation.
4//!
5//! # Example
6//!
7//! ```rust,ignore
8//! use heliosdb::proxy::multi_tenancy::{
9//!     TenantConfig, IsolationStrategy, TenantPoolConfig, TenantRateLimits,
10//! };
11//!
12//! let config = TenantConfig::builder()
13//!     .id("tenant_a")
14//!     .name("Acme Corp")
15//!     .isolation(IsolationStrategy::Schema {
16//!         database_name: "shared_db".to_string(),
17//!         schema_name: "tenant_a".to_string(),
18//!     })
19//!     .max_connections(50)
20//!     .qps_limit(1000)
21//!     .build();
22//! ```
23
24use std::collections::HashMap;
25use std::time::Duration;
26
27/// Unique tenant identifier
28#[derive(Debug, Clone, PartialEq, Eq, Hash)]
29pub struct TenantId(pub String);
30
31impl TenantId {
32    /// Create a new tenant ID
33    pub fn new(id: impl Into<String>) -> Self {
34        Self(id.into())
35    }
36
37    /// Get the tenant ID as a string slice
38    pub fn as_str(&self) -> &str {
39        &self.0
40    }
41}
42
43impl std::fmt::Display for TenantId {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        write!(f, "{}", self.0)
46    }
47}
48
49impl From<&str> for TenantId {
50    fn from(s: &str) -> Self {
51        Self(s.to_string())
52    }
53}
54
55impl From<String> for TenantId {
56    fn from(s: String) -> Self {
57        Self(s)
58    }
59}
60
61/// Tenant isolation strategy
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub enum IsolationStrategy {
64    /// Separate database per tenant
65    Database {
66        /// Database name for this tenant
67        database_name: String,
68    },
69
70    /// Separate schema per tenant (same database)
71    Schema {
72        /// Database name containing the schema
73        database_name: String,
74        /// Schema name for this tenant
75        schema_name: String,
76    },
77
78    /// Row-level security (same tables)
79    Row {
80        /// Database name
81        database_name: String,
82        /// Column name containing tenant ID
83        tenant_column: String,
84    },
85
86    /// Branch per tenant (HeliosDB-Lite specific)
87    Branch {
88        /// Branch name for this tenant
89        branch_name: String,
90    },
91}
92
93impl IsolationStrategy {
94    /// Create database isolation strategy
95    pub fn database(name: impl Into<String>) -> Self {
96        Self::Database {
97            database_name: name.into(),
98        }
99    }
100
101    /// Create schema isolation strategy
102    pub fn schema(database: impl Into<String>, schema: impl Into<String>) -> Self {
103        Self::Schema {
104            database_name: database.into(),
105            schema_name: schema.into(),
106        }
107    }
108
109    /// Create row-level isolation strategy
110    pub fn row(database: impl Into<String>, column: impl Into<String>) -> Self {
111        Self::Row {
112            database_name: database.into(),
113            tenant_column: column.into(),
114        }
115    }
116
117    /// Create branch isolation strategy
118    pub fn branch(name: impl Into<String>) -> Self {
119        Self::Branch {
120            branch_name: name.into(),
121        }
122    }
123
124    /// Get the database name for this strategy
125    pub fn database_name(&self) -> Option<&str> {
126        match self {
127            Self::Database { database_name } => Some(database_name),
128            Self::Schema { database_name, .. } => Some(database_name),
129            Self::Row { database_name, .. } => Some(database_name),
130            Self::Branch { .. } => None,
131        }
132    }
133
134    /// Get the schema name if using schema isolation
135    pub fn schema_name(&self) -> Option<&str> {
136        match self {
137            Self::Schema { schema_name, .. } => Some(schema_name),
138            _ => None,
139        }
140    }
141
142    /// Get the tenant column if using row-level isolation
143    pub fn tenant_column(&self) -> Option<&str> {
144        match self {
145            Self::Row { tenant_column, .. } => Some(tenant_column),
146            _ => None,
147        }
148    }
149
150    /// Get the branch name if using branch isolation
151    pub fn branch_name(&self) -> Option<&str> {
152        match self {
153            Self::Branch { branch_name } => Some(branch_name),
154            _ => None,
155        }
156    }
157
158    /// Check if this strategy requires query transformation
159    pub fn requires_query_transform(&self) -> bool {
160        matches!(self, Self::Row { .. })
161    }
162
163    /// Check if this strategy requires connection routing
164    pub fn requires_connection_routing(&self) -> bool {
165        matches!(self, Self::Database { .. } | Self::Branch { .. })
166    }
167
168    /// Get display name for this strategy
169    pub fn strategy_name(&self) -> &'static str {
170        match self {
171            Self::Database { .. } => "database",
172            Self::Schema { .. } => "schema",
173            Self::Row { .. } => "row",
174            Self::Branch { .. } => "branch",
175        }
176    }
177}
178
179/// Tenant-specific rate limits
180#[derive(Debug, Clone)]
181pub struct TenantRateLimits {
182    /// Maximum queries per second
183    pub qps_limit: u32,
184
185    /// Maximum concurrent connections
186    pub max_connections: u32,
187
188    /// Maximum query duration before kill
189    pub max_query_duration: Duration,
190
191    /// Maximum result size (bytes)
192    pub max_result_size: u64,
193
194    /// Maximum rows per query
195    pub max_rows_per_query: u64,
196
197    /// Burst allowance (multiplier over qps_limit for short bursts)
198    pub burst_multiplier: f32,
199}
200
201impl Default for TenantRateLimits {
202    fn default() -> Self {
203        Self {
204            qps_limit: 100,
205            max_connections: 10,
206            max_query_duration: Duration::from_secs(60),
207            max_result_size: 100 * 1024 * 1024, // 100MB
208            max_rows_per_query: 100_000,
209            burst_multiplier: 2.0,
210        }
211    }
212}
213
214impl TenantRateLimits {
215    /// Create new rate limits with QPS limit
216    pub fn with_qps(qps: u32) -> Self {
217        Self {
218            qps_limit: qps,
219            ..Default::default()
220        }
221    }
222
223    /// Set the QPS limit
224    pub fn qps_limit(mut self, limit: u32) -> Self {
225        self.qps_limit = limit;
226        self
227    }
228
229    /// Set the max connections
230    pub fn max_connections(mut self, limit: u32) -> Self {
231        self.max_connections = limit;
232        self
233    }
234
235    /// Set the max query duration
236    pub fn max_query_duration(mut self, duration: Duration) -> Self {
237        self.max_query_duration = duration;
238        self
239    }
240
241    /// Set the burst multiplier
242    pub fn burst_multiplier(mut self, multiplier: f32) -> Self {
243        self.burst_multiplier = multiplier;
244        self
245    }
246}
247
248/// Tenant-specific connection pool configuration
249#[derive(Debug, Clone)]
250pub struct TenantPoolConfig {
251    /// Maximum connections in pool
252    pub max_connections: u32,
253
254    /// Minimum idle connections
255    pub min_idle: u32,
256
257    /// Idle connection timeout
258    pub idle_timeout: Duration,
259
260    /// Maximum connection lifetime
261    pub max_lifetime: Duration,
262
263    /// Connection acquire timeout
264    pub acquire_timeout: Duration,
265
266    /// Whether this tenant uses dedicated pool
267    pub dedicated_pool: bool,
268}
269
270impl Default for TenantPoolConfig {
271    fn default() -> Self {
272        Self {
273            max_connections: 10,
274            min_idle: 1,
275            idle_timeout: Duration::from_secs(600),
276            max_lifetime: Duration::from_secs(3600),
277            acquire_timeout: Duration::from_secs(5),
278            dedicated_pool: false,
279        }
280    }
281}
282
283impl TenantPoolConfig {
284    /// Create pool config with max connections
285    pub fn with_max_connections(max: u32) -> Self {
286        Self {
287            max_connections: max,
288            ..Default::default()
289        }
290    }
291
292    /// Set dedicated pool flag
293    pub fn dedicated(mut self) -> Self {
294        self.dedicated_pool = true;
295        self
296    }
297
298    /// Set min idle connections
299    pub fn min_idle(mut self, min: u32) -> Self {
300        self.min_idle = min;
301        self
302    }
303
304    /// Set idle timeout
305    pub fn idle_timeout(mut self, timeout: Duration) -> Self {
306        self.idle_timeout = timeout;
307        self
308    }
309}
310
311/// Tenant permissions and restrictions
312#[derive(Debug, Clone)]
313pub struct TenantPermissions {
314    /// Allowed SQL operations (SELECT, INSERT, UPDATE, DELETE, DDL)
315    pub allowed_operations: Vec<String>,
316
317    /// Blocked tables (cannot access)
318    pub blocked_tables: Vec<String>,
319
320    /// Read-only mode
321    pub read_only: bool,
322
323    /// Can execute DDL statements
324    pub allow_ddl: bool,
325
326    /// Can execute EXPLAIN/ANALYZE
327    pub allow_explain: bool,
328
329    /// Can access system tables
330    pub allow_system_access: bool,
331
332    /// Maximum tables per query (for complexity limiting)
333    pub max_tables_per_query: u32,
334}
335
336impl Default for TenantPermissions {
337    fn default() -> Self {
338        Self {
339            allowed_operations: vec![
340                "SELECT".to_string(),
341                "INSERT".to_string(),
342                "UPDATE".to_string(),
343                "DELETE".to_string(),
344            ],
345            blocked_tables: vec![],
346            read_only: false,
347            allow_ddl: false,
348            allow_explain: true,
349            allow_system_access: false,
350            max_tables_per_query: 10,
351        }
352    }
353}
354
355impl TenantPermissions {
356    /// Create read-only permissions
357    pub fn read_only() -> Self {
358        Self {
359            allowed_operations: vec!["SELECT".to_string()],
360            read_only: true,
361            ..Default::default()
362        }
363    }
364
365    /// Create full access permissions
366    pub fn full_access() -> Self {
367        Self {
368            allowed_operations: vec![
369                "SELECT".to_string(),
370                "INSERT".to_string(),
371                "UPDATE".to_string(),
372                "DELETE".to_string(),
373                "CREATE".to_string(),
374                "ALTER".to_string(),
375                "DROP".to_string(),
376            ],
377            allow_ddl: true,
378            allow_system_access: true,
379            ..Default::default()
380        }
381    }
382
383    /// Check if operation is allowed
384    pub fn is_operation_allowed(&self, operation: &str) -> bool {
385        self.allowed_operations
386            .iter()
387            .any(|op| op.eq_ignore_ascii_case(operation))
388    }
389
390    /// Check if table access is allowed
391    pub fn is_table_allowed(&self, table: &str) -> bool {
392        !self
393            .blocked_tables
394            .iter()
395            .any(|t| t.eq_ignore_ascii_case(table))
396    }
397}
398
399/// AI workload configuration per tenant
400#[derive(Debug, Clone)]
401pub struct TenantAiConfig {
402    /// Knowledge base identifier
403    pub knowledge_base: Option<String>,
404
405    /// Embedding model to use
406    pub embedding_model: String,
407
408    /// Maximum retrieval results
409    pub retrieval_limit: u32,
410
411    /// Token budget per day
412    pub daily_token_budget: Option<u64>,
413
414    /// Enable agent workspace
415    pub agent_workspace_enabled: bool,
416
417    /// Maximum concurrent agents
418    pub max_concurrent_agents: u32,
419}
420
421impl Default for TenantAiConfig {
422    fn default() -> Self {
423        Self {
424            knowledge_base: None,
425            embedding_model: "default".to_string(),
426            retrieval_limit: 10,
427            daily_token_budget: None,
428            agent_workspace_enabled: true,
429            max_concurrent_agents: 5,
430        }
431    }
432}
433
434/// Full tenant configuration
435#[derive(Debug, Clone)]
436pub struct TenantConfig {
437    /// Tenant identifier
438    pub id: TenantId,
439
440    /// Display name
441    pub name: String,
442
443    /// Isolation strategy
444    pub isolation: IsolationStrategy,
445
446    /// Rate limits
447    pub rate_limits: TenantRateLimits,
448
449    /// Connection pool settings
450    pub pool: TenantPoolConfig,
451
452    /// Permissions and restrictions
453    pub permissions: TenantPermissions,
454
455    /// AI workload configuration
456    pub ai_config: TenantAiConfig,
457
458    /// Custom metadata
459    pub metadata: HashMap<String, String>,
460
461    /// Whether tenant is enabled
462    pub enabled: bool,
463
464    /// Tenant creation timestamp
465    pub created_at: std::time::SystemTime,
466}
467
468impl TenantConfig {
469    /// Create a new tenant config builder
470    pub fn builder() -> TenantConfigBuilder {
471        TenantConfigBuilder::new()
472    }
473
474    /// Create with minimal configuration
475    pub fn new(id: impl Into<TenantId>, isolation: IsolationStrategy) -> Self {
476        Self {
477            id: id.into(),
478            name: String::new(),
479            isolation,
480            rate_limits: TenantRateLimits::default(),
481            pool: TenantPoolConfig::default(),
482            permissions: TenantPermissions::default(),
483            ai_config: TenantAiConfig::default(),
484            metadata: HashMap::new(),
485            enabled: true,
486            created_at: std::time::SystemTime::now(),
487        }
488    }
489
490    /// Check if tenant is in a healthy state
491    pub fn is_healthy(&self) -> bool {
492        self.enabled
493    }
494
495    /// Get effective max connections considering pool config
496    pub fn effective_max_connections(&self) -> u32 {
497        self.pool
498            .max_connections
499            .min(self.rate_limits.max_connections)
500    }
501}
502
503/// Builder for TenantConfig
504#[derive(Debug, Default)]
505pub struct TenantConfigBuilder {
506    id: Option<TenantId>,
507    name: Option<String>,
508    isolation: Option<IsolationStrategy>,
509    rate_limits: Option<TenantRateLimits>,
510    pool: Option<TenantPoolConfig>,
511    permissions: Option<TenantPermissions>,
512    ai_config: Option<TenantAiConfig>,
513    metadata: HashMap<String, String>,
514    enabled: bool,
515}
516
517impl TenantConfigBuilder {
518    /// Create a new builder
519    pub fn new() -> Self {
520        Self {
521            enabled: true,
522            ..Default::default()
523        }
524    }
525
526    /// Set tenant ID
527    pub fn id(mut self, id: impl Into<TenantId>) -> Self {
528        self.id = Some(id.into());
529        self
530    }
531
532    /// Set tenant name
533    pub fn name(mut self, name: impl Into<String>) -> Self {
534        self.name = Some(name.into());
535        self
536    }
537
538    /// Set isolation strategy
539    pub fn isolation(mut self, strategy: IsolationStrategy) -> Self {
540        self.isolation = Some(strategy);
541        self
542    }
543
544    /// Set database isolation
545    pub fn database_isolation(self, database: impl Into<String>) -> Self {
546        self.isolation(IsolationStrategy::database(database))
547    }
548
549    /// Set schema isolation
550    pub fn schema_isolation(self, database: impl Into<String>, schema: impl Into<String>) -> Self {
551        self.isolation(IsolationStrategy::schema(database, schema))
552    }
553
554    /// Set row-level isolation
555    pub fn row_isolation(self, database: impl Into<String>, column: impl Into<String>) -> Self {
556        self.isolation(IsolationStrategy::row(database, column))
557    }
558
559    /// Set branch isolation
560    pub fn branch_isolation(self, branch: impl Into<String>) -> Self {
561        self.isolation(IsolationStrategy::branch(branch))
562    }
563
564    /// Set rate limits
565    pub fn rate_limits(mut self, limits: TenantRateLimits) -> Self {
566        self.rate_limits = Some(limits);
567        self
568    }
569
570    /// Set QPS limit
571    pub fn qps_limit(mut self, limit: u32) -> Self {
572        let mut limits = self.rate_limits.take().unwrap_or_default();
573        limits.qps_limit = limit;
574        self.rate_limits = Some(limits);
575        self
576    }
577
578    /// Set max connections
579    pub fn max_connections(mut self, max: u32) -> Self {
580        let mut pool = self.pool.take().unwrap_or_default();
581        pool.max_connections = max;
582        self.pool = Some(pool);
583
584        let mut limits = self.rate_limits.take().unwrap_or_default();
585        limits.max_connections = max;
586        self.rate_limits = Some(limits);
587        self
588    }
589
590    /// Set pool configuration
591    pub fn pool(mut self, config: TenantPoolConfig) -> Self {
592        self.pool = Some(config);
593        self
594    }
595
596    /// Set permissions
597    pub fn permissions(mut self, perms: TenantPermissions) -> Self {
598        self.permissions = Some(perms);
599        self
600    }
601
602    /// Set read-only mode
603    pub fn read_only(self) -> Self {
604        self.permissions(TenantPermissions::read_only())
605    }
606
607    /// Set AI configuration
608    pub fn ai_config(mut self, config: TenantAiConfig) -> Self {
609        self.ai_config = Some(config);
610        self
611    }
612
613    /// Add metadata
614    pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
615        self.metadata.insert(key.into(), value.into());
616        self
617    }
618
619    /// Set enabled state
620    pub fn enabled(mut self, enabled: bool) -> Self {
621        self.enabled = enabled;
622        self
623    }
624
625    /// Build the TenantConfig
626    pub fn build(self) -> TenantConfig {
627        TenantConfig {
628            id: self.id.expect("tenant id is required"),
629            name: self.name.unwrap_or_default(),
630            isolation: self.isolation.expect("isolation strategy is required"),
631            rate_limits: self.rate_limits.unwrap_or_default(),
632            pool: self.pool.unwrap_or_default(),
633            permissions: self.permissions.unwrap_or_default(),
634            ai_config: self.ai_config.unwrap_or_default(),
635            metadata: self.metadata,
636            enabled: self.enabled,
637            created_at: std::time::SystemTime::now(),
638        }
639    }
640}
641
642/// How tenants are identified from requests
643#[derive(Debug, Clone)]
644pub enum IdentificationMethod {
645    /// Extract from HTTP header
646    Header {
647        /// Header name (e.g., "X-Tenant-Id")
648        header_name: String,
649    },
650
651    /// Extract from username prefix
652    UsernamePrefix {
653        /// Separator character (e.g., '.')
654        separator: char,
655    },
656
657    /// Extract from JWT claim
658    JwtClaim {
659        /// Claim name (e.g., "tenant_id")
660        claim_name: String,
661        /// JWT issuer for validation
662        issuer: Option<String>,
663    },
664
665    /// Extract from database name
666    DatabaseName,
667
668    /// SQL context variable
669    SqlContext {
670        /// Variable name (e.g., "helios.tenant_id")
671        variable_name: String,
672    },
673}
674
675impl Default for IdentificationMethod {
676    fn default() -> Self {
677        Self::Header {
678            header_name: "X-Tenant-Id".to_string(),
679        }
680    }
681}
682
683impl IdentificationMethod {
684    /// Create header identification
685    pub fn header(name: impl Into<String>) -> Self {
686        Self::Header {
687            header_name: name.into(),
688        }
689    }
690
691    /// Create username prefix identification
692    pub fn username_prefix(separator: char) -> Self {
693        Self::UsernamePrefix { separator }
694    }
695
696    /// Create JWT claim identification
697    pub fn jwt_claim(claim: impl Into<String>) -> Self {
698        Self::JwtClaim {
699            claim_name: claim.into(),
700            issuer: None,
701        }
702    }
703
704    /// Create database name identification
705    pub fn database_name() -> Self {
706        Self::DatabaseName
707    }
708
709    /// Create SQL context identification
710    pub fn sql_context(variable: impl Into<String>) -> Self {
711        Self::SqlContext {
712            variable_name: variable.into(),
713        }
714    }
715}
716
717/// Global multi-tenancy configuration
718#[derive(Debug, Clone)]
719pub struct MultiTenancyConfig {
720    /// Whether multi-tenancy is enabled
721    pub enabled: bool,
722
723    /// How to identify tenants
724    pub identification: IdentificationMethod,
725
726    /// Default tenant configuration
727    pub default_config: TenantConfig,
728
729    /// Whether to allow unknown tenants
730    pub allow_unknown_tenants: bool,
731
732    /// Whether to create tenants on-demand
733    pub auto_create_tenants: bool,
734
735    /// Maximum tenants allowed
736    pub max_tenants: u32,
737
738    /// Enable cross-tenant analytics for admins
739    pub cross_tenant_analytics: bool,
740
741    /// Admin user pattern (for cross-tenant access)
742    pub admin_user_pattern: Option<String>,
743}
744
745impl Default for MultiTenancyConfig {
746    fn default() -> Self {
747        Self {
748            enabled: false,
749            identification: IdentificationMethod::default(),
750            default_config: TenantConfig::new(
751                TenantId::new("default"),
752                IsolationStrategy::schema("public", "public"),
753            ),
754            allow_unknown_tenants: false,
755            auto_create_tenants: false,
756            max_tenants: 1000,
757            cross_tenant_analytics: false,
758            admin_user_pattern: None,
759        }
760    }
761}
762
763impl MultiTenancyConfig {
764    /// Create enabled multi-tenancy config
765    pub fn enabled() -> Self {
766        Self {
767            enabled: true,
768            ..Default::default()
769        }
770    }
771
772    /// Set identification method
773    pub fn with_identification(mut self, method: IdentificationMethod) -> Self {
774        self.identification = method;
775        self
776    }
777
778    /// Set default tenant config
779    pub fn with_default_config(mut self, config: TenantConfig) -> Self {
780        self.default_config = config;
781        self
782    }
783
784    /// Allow unknown tenants
785    pub fn allow_unknown(mut self) -> Self {
786        self.allow_unknown_tenants = true;
787        self
788    }
789
790    /// Enable auto-creation of tenants
791    pub fn auto_create(mut self) -> Self {
792        self.auto_create_tenants = true;
793        self
794    }
795}
796
797#[cfg(test)]
798mod tests {
799    use super::*;
800
801    #[test]
802    fn test_tenant_id() {
803        let id = TenantId::new("test_tenant");
804        assert_eq!(id.as_str(), "test_tenant");
805        assert_eq!(id.to_string(), "test_tenant");
806
807        let id2: TenantId = "another".into();
808        assert_eq!(id2.as_str(), "another");
809    }
810
811    #[test]
812    fn test_isolation_strategy() {
813        let db = IsolationStrategy::database("mydb");
814        assert_eq!(db.database_name(), Some("mydb"));
815        assert_eq!(db.strategy_name(), "database");
816        assert!(db.requires_connection_routing());
817        assert!(!db.requires_query_transform());
818
819        let schema = IsolationStrategy::schema("mydb", "myschema");
820        assert_eq!(schema.database_name(), Some("mydb"));
821        assert_eq!(schema.schema_name(), Some("myschema"));
822        assert_eq!(schema.strategy_name(), "schema");
823
824        let row = IsolationStrategy::row("mydb", "tenant_id");
825        assert_eq!(row.tenant_column(), Some("tenant_id"));
826        assert!(row.requires_query_transform());
827
828        let branch = IsolationStrategy::branch("tenant_branch");
829        assert_eq!(branch.branch_name(), Some("tenant_branch"));
830        assert!(branch.requires_connection_routing());
831    }
832
833    #[test]
834    fn test_tenant_config_builder() {
835        let config = TenantConfig::builder()
836            .id("tenant_a")
837            .name("Acme Corp")
838            .schema_isolation("shared_db", "tenant_a")
839            .max_connections(50)
840            .qps_limit(1000)
841            .metadata("tier", "enterprise")
842            .build();
843
844        assert_eq!(config.id.as_str(), "tenant_a");
845        assert_eq!(config.name, "Acme Corp");
846        assert_eq!(config.pool.max_connections, 50);
847        assert_eq!(config.rate_limits.qps_limit, 1000);
848        assert_eq!(config.metadata.get("tier"), Some(&"enterprise".to_string()));
849    }
850
851    #[test]
852    fn test_tenant_permissions() {
853        let default = TenantPermissions::default();
854        assert!(default.is_operation_allowed("SELECT"));
855        assert!(default.is_operation_allowed("select"));
856        assert!(!default.is_operation_allowed("CREATE"));
857        assert!(!default.allow_ddl);
858
859        let read_only = TenantPermissions::read_only();
860        assert!(read_only.is_operation_allowed("SELECT"));
861        assert!(!read_only.is_operation_allowed("INSERT"));
862        assert!(read_only.read_only);
863
864        let full = TenantPermissions::full_access();
865        assert!(full.is_operation_allowed("CREATE"));
866        assert!(full.allow_ddl);
867    }
868
869    #[test]
870    fn test_identification_methods() {
871        let header = IdentificationMethod::header("X-Tenant-Id");
872        assert!(
873            matches!(header, IdentificationMethod::Header { header_name } if header_name == "X-Tenant-Id")
874        );
875
876        let prefix = IdentificationMethod::username_prefix('.');
877        assert!(matches!(
878            prefix,
879            IdentificationMethod::UsernamePrefix { separator: '.' }
880        ));
881
882        let jwt = IdentificationMethod::jwt_claim("tenant_id");
883        assert!(
884            matches!(jwt, IdentificationMethod::JwtClaim { claim_name, .. } if claim_name == "tenant_id")
885        );
886    }
887
888    #[test]
889    fn test_multi_tenancy_config() {
890        let config = MultiTenancyConfig::enabled()
891            .with_identification(IdentificationMethod::header("X-Org-Id"))
892            .allow_unknown()
893            .auto_create();
894
895        assert!(config.enabled);
896        assert!(config.allow_unknown_tenants);
897        assert!(config.auto_create_tenants);
898    }
899}