Skip to main content

static_tr_plugin/domain/
service.rs

1//! Domain service for the static tenant resolver plugin.
2
3use std::collections::{HashMap, HashSet};
4
5use tenant_resolver_sdk::{TenantFilter, TenantId, TenantInfo};
6
7use crate::config::StaticTrPluginConfig;
8
9/// Static tenant resolver service.
10///
11/// Stores tenant data and access rules in memory, loaded from configuration.
12pub struct Service {
13    /// Tenant info by ID.
14    pub(super) tenants: HashMap<TenantId, TenantInfo>,
15
16    /// Access rules: set of (source, target) pairs.
17    pub(super) access_rules: HashSet<(TenantId, TenantId)>,
18
19    /// Reverse index: target -> list of sources that can access it.
20    /// Used for efficient `get_accessible_tenants`.
21    pub(super) accessible_by: HashMap<TenantId, Vec<TenantId>>,
22}
23
24impl Service {
25    /// Creates a new service from configuration.
26    #[must_use]
27    pub fn from_config(cfg: &StaticTrPluginConfig) -> Self {
28        let tenants: HashMap<TenantId, TenantInfo> = cfg
29            .tenants
30            .iter()
31            .map(|t| {
32                (
33                    t.id,
34                    TenantInfo {
35                        id: t.id,
36                        name: t.name.clone(),
37                        status: t.status,
38                        tenant_type: t.tenant_type.clone(),
39                    },
40                )
41            })
42            .collect();
43
44        let access_rules: HashSet<(TenantId, TenantId)> = cfg
45            .access_rules
46            .iter()
47            .map(|r| (r.source, r.target))
48            .collect();
49
50        // Build reverse index: for each source, which targets can it access?
51        let mut accessible_by: HashMap<TenantId, Vec<TenantId>> = HashMap::new();
52        for (source, target) in &access_rules {
53            accessible_by.entry(*source).or_default().push(*target);
54        }
55
56        Self {
57            tenants,
58            access_rules,
59            accessible_by,
60        }
61    }
62
63    /// Check if a tenant matches the filter criteria.
64    pub(super) fn matches_filter(tenant: &TenantInfo, filter: Option<&TenantFilter>) -> bool {
65        let Some(filter) = filter else {
66            return true;
67        };
68
69        // Check ID filter
70        if !filter.id.is_empty() && !filter.id.contains(&tenant.id) {
71            return false;
72        }
73
74        // Check status filter
75        if !filter.status.is_empty() && !filter.status.contains(&tenant.status) {
76            return false;
77        }
78
79        true
80    }
81}
82
83#[cfg(test)]
84#[cfg_attr(coverage_nightly, coverage(off))]
85mod tests {
86    use super::*;
87    use crate::config::{AccessRuleConfig, TenantConfig};
88    use tenant_resolver_sdk::TenantStatus;
89    use uuid::Uuid;
90
91    // Helper to create a test tenant config
92    fn tenant(id: &str, name: &str, status: TenantStatus) -> TenantConfig {
93        TenantConfig {
94            id: Uuid::parse_str(id).unwrap(),
95            name: name.to_owned(),
96            status,
97            tenant_type: None,
98        }
99    }
100
101    // Helper to create an access rule config
102    fn access_rule(source: &str, target: &str) -> AccessRuleConfig {
103        AccessRuleConfig {
104            source: Uuid::parse_str(source).unwrap(),
105            target: Uuid::parse_str(target).unwrap(),
106        }
107    }
108
109    // Test UUIDs
110    const TENANT_A: &str = "11111111-1111-1111-1111-111111111111";
111    const TENANT_B: &str = "22222222-2222-2222-2222-222222222222";
112    const TENANT_C: &str = "33333333-3333-3333-3333-333333333333";
113
114    // ==================== from_config tests ====================
115
116    #[test]
117    fn from_config_empty() {
118        let cfg = StaticTrPluginConfig::default();
119        let service = Service::from_config(&cfg);
120
121        assert!(service.tenants.is_empty());
122        assert!(service.access_rules.is_empty());
123        assert!(service.accessible_by.is_empty());
124    }
125
126    #[test]
127    fn from_config_with_tenants_only() {
128        let cfg = StaticTrPluginConfig {
129            tenants: vec![
130                tenant(TENANT_A, "Tenant A", TenantStatus::Active),
131                tenant(TENANT_B, "Tenant B", TenantStatus::Suspended),
132            ],
133            ..Default::default()
134        };
135        let service = Service::from_config(&cfg);
136
137        assert_eq!(service.tenants.len(), 2);
138        assert!(service.access_rules.is_empty());
139        assert!(service.accessible_by.is_empty());
140
141        let a = service
142            .tenants
143            .get(&Uuid::parse_str(TENANT_A).unwrap())
144            .unwrap();
145        assert_eq!(a.name, "Tenant A");
146        assert_eq!(a.status, TenantStatus::Active);
147
148        let b = service
149            .tenants
150            .get(&Uuid::parse_str(TENANT_B).unwrap())
151            .unwrap();
152        assert_eq!(b.name, "Tenant B");
153        assert_eq!(b.status, TenantStatus::Suspended);
154    }
155
156    #[test]
157    fn from_config_with_access_rules() {
158        let cfg = StaticTrPluginConfig {
159            tenants: vec![
160                tenant(TENANT_A, "A", TenantStatus::Active),
161                tenant(TENANT_B, "B", TenantStatus::Active),
162                tenant(TENANT_C, "C", TenantStatus::Active),
163            ],
164            access_rules: vec![
165                access_rule(TENANT_A, TENANT_B), // A can access B
166                access_rule(TENANT_A, TENANT_C), // A can access C
167                access_rule(TENANT_B, TENANT_C), // B can access C
168            ],
169            ..Default::default()
170        };
171        let service = Service::from_config(&cfg);
172
173        assert_eq!(service.access_rules.len(), 3);
174
175        // Check reverse index
176        let a_id = Uuid::parse_str(TENANT_A).unwrap();
177        let b_id = Uuid::parse_str(TENANT_B).unwrap();
178        let c_id = Uuid::parse_str(TENANT_C).unwrap();
179
180        let a_accessible = service.accessible_by.get(&a_id).unwrap();
181        assert_eq!(a_accessible.len(), 2);
182        assert!(a_accessible.contains(&b_id));
183        assert!(a_accessible.contains(&c_id));
184
185        let b_accessible = service.accessible_by.get(&b_id).unwrap();
186        assert_eq!(b_accessible.len(), 1);
187        assert!(b_accessible.contains(&c_id));
188
189        // C has no access rules
190        assert!(!service.accessible_by.contains_key(&c_id));
191    }
192
193    #[test]
194    fn from_config_with_tenant_type() {
195        let cfg = StaticTrPluginConfig {
196            tenants: vec![TenantConfig {
197                id: Uuid::parse_str(TENANT_A).unwrap(),
198                name: "Enterprise".to_owned(),
199                status: TenantStatus::Active,
200                tenant_type: Some("enterprise".to_owned()),
201            }],
202            ..Default::default()
203        };
204        let service = Service::from_config(&cfg);
205
206        let a = service
207            .tenants
208            .get(&Uuid::parse_str(TENANT_A).unwrap())
209            .unwrap();
210        assert_eq!(a.tenant_type, Some("enterprise".to_owned()));
211    }
212}