mockforge_federation/
federation.rs

1//! Federation definition and management
2//!
3//! A federation is a collection of services that form a virtual system.
4
5use crate::service::{ServiceBoundary, ServiceRealityLevel};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use uuid::Uuid;
10
11/// Federation configuration
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct FederationConfig {
14    /// Federation name
15    pub name: String,
16    /// Federation description
17    #[serde(default)]
18    pub description: String,
19    /// Services in this federation
20    pub services: Vec<FederationService>,
21}
22
23/// Service definition in federation
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct FederationService {
26    /// Service name
27    pub name: String,
28    /// Workspace ID
29    pub workspace_id: String, // UUID as string for YAML
30    /// Base path
31    pub base_path: String,
32    /// Reality level
33    pub reality_level: String,
34    /// Service-specific config
35    #[serde(default)]
36    pub config: HashMap<String, serde_json::Value>,
37    /// Dependencies
38    #[serde(default)]
39    pub dependencies: Vec<String>,
40}
41
42/// Federation metadata (stored in database)
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Federation {
45    /// Federation ID
46    pub id: Uuid,
47    /// Federation name
48    pub name: String,
49    /// Federation description
50    pub description: String,
51    /// Organization ID
52    pub org_id: Uuid,
53    /// Service boundaries
54    pub services: Vec<ServiceBoundary>,
55    /// Created timestamp
56    pub created_at: DateTime<Utc>,
57    /// Updated timestamp
58    pub updated_at: DateTime<Utc>,
59}
60
61impl Federation {
62    /// Create a new federation from config
63    pub fn from_config(org_id: Uuid, config: FederationConfig) -> Result<Self, String> {
64        let mut services = Vec::new();
65
66        for service_config in config.services {
67            let workspace_id = Uuid::parse_str(&service_config.workspace_id)
68                .map_err(|_| format!("Invalid workspace_id: {}", service_config.workspace_id))?;
69
70            let reality_level = ServiceRealityLevel::from_str(&service_config.reality_level)
71                .ok_or_else(|| {
72                    format!("Invalid reality_level: {}", service_config.reality_level)
73                })?;
74
75            let mut service = ServiceBoundary::new(
76                service_config.name.clone(),
77                workspace_id,
78                service_config.base_path.clone(),
79                reality_level,
80            );
81
82            service.config = service_config.config;
83            service.dependencies = service_config.dependencies;
84
85            services.push(service);
86        }
87
88        let now = Utc::now();
89
90        Ok(Self {
91            id: Uuid::new_v4(),
92            name: config.name,
93            description: config.description,
94            org_id,
95            services,
96            created_at: now,
97            updated_at: now,
98        })
99    }
100
101    /// Find service by path
102    #[must_use]
103    pub fn find_service_by_path(&self, path: &str) -> Option<&ServiceBoundary> {
104        // Find the longest matching base_path (most specific match)
105        self.services
106            .iter()
107            .filter(|s| s.matches_path(path))
108            .max_by_key(|s| s.base_path.len())
109    }
110
111    /// Get service by name
112    #[must_use]
113    pub fn get_service(&self, name: &str) -> Option<&ServiceBoundary> {
114        self.services.iter().find(|s| s.name == name)
115    }
116
117    /// Add a service to the federation
118    pub fn add_service(&mut self, service: ServiceBoundary) {
119        self.services.push(service);
120        self.updated_at = Utc::now();
121    }
122
123    /// Remove a service from the federation
124    pub fn remove_service(&mut self, name: &str) -> bool {
125        let len_before = self.services.len();
126        self.services.retain(|s| s.name != name);
127        let removed = self.services.len() < len_before;
128        if removed {
129            self.updated_at = Utc::now();
130        }
131        removed
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    fn create_test_federation() -> Federation {
140        Federation {
141            id: Uuid::new_v4(),
142            name: "test".to_string(),
143            description: "Test federation".to_string(),
144            org_id: Uuid::new_v4(),
145            services: vec![
146                ServiceBoundary::new(
147                    "auth".to_string(),
148                    Uuid::new_v4(),
149                    "/auth".to_string(),
150                    ServiceRealityLevel::Real,
151                ),
152                ServiceBoundary::new(
153                    "payments".to_string(),
154                    Uuid::new_v4(),
155                    "/payments".to_string(),
156                    ServiceRealityLevel::MockV3,
157                ),
158            ],
159            created_at: Utc::now(),
160            updated_at: Utc::now(),
161        }
162    }
163
164    #[test]
165    fn test_federation_from_config() {
166        let config = FederationConfig {
167            name: "test-federation".to_string(),
168            description: "Test".to_string(),
169            services: vec![FederationService {
170                name: "auth".to_string(),
171                workspace_id: Uuid::new_v4().to_string(),
172                base_path: "/auth".to_string(),
173                reality_level: "real".to_string(),
174                config: HashMap::new(),
175                dependencies: Vec::new(),
176            }],
177        };
178
179        let federation = Federation::from_config(Uuid::new_v4(), config).unwrap();
180        assert_eq!(federation.services.len(), 1);
181        assert_eq!(federation.services[0].name, "auth");
182    }
183
184    #[test]
185    fn test_federation_from_config_multiple_services() {
186        let config = FederationConfig {
187            name: "multi-service".to_string(),
188            description: "Multiple services".to_string(),
189            services: vec![
190                FederationService {
191                    name: "auth".to_string(),
192                    workspace_id: Uuid::new_v4().to_string(),
193                    base_path: "/auth".to_string(),
194                    reality_level: "real".to_string(),
195                    config: HashMap::new(),
196                    dependencies: Vec::new(),
197                },
198                FederationService {
199                    name: "payments".to_string(),
200                    workspace_id: Uuid::new_v4().to_string(),
201                    base_path: "/payments".to_string(),
202                    reality_level: "mock_v3".to_string(),
203                    config: HashMap::new(),
204                    dependencies: vec!["auth".to_string()],
205                },
206            ],
207        };
208
209        let federation = Federation::from_config(Uuid::new_v4(), config).unwrap();
210        assert_eq!(federation.services.len(), 2);
211        assert_eq!(federation.services[1].dependencies, vec!["auth".to_string()]);
212    }
213
214    #[test]
215    fn test_federation_from_config_invalid_workspace_id() {
216        let config = FederationConfig {
217            name: "test".to_string(),
218            description: String::new(),
219            services: vec![FederationService {
220                name: "auth".to_string(),
221                workspace_id: "invalid-uuid".to_string(),
222                base_path: "/auth".to_string(),
223                reality_level: "real".to_string(),
224                config: HashMap::new(),
225                dependencies: Vec::new(),
226            }],
227        };
228
229        let result = Federation::from_config(Uuid::new_v4(), config);
230        assert!(result.is_err());
231        assert!(result.unwrap_err().contains("Invalid workspace_id"));
232    }
233
234    #[test]
235    fn test_federation_from_config_invalid_reality_level() {
236        let config = FederationConfig {
237            name: "test".to_string(),
238            description: String::new(),
239            services: vec![FederationService {
240                name: "auth".to_string(),
241                workspace_id: Uuid::new_v4().to_string(),
242                base_path: "/auth".to_string(),
243                reality_level: "invalid_level".to_string(),
244                config: HashMap::new(),
245                dependencies: Vec::new(),
246            }],
247        };
248
249        let result = Federation::from_config(Uuid::new_v4(), config);
250        assert!(result.is_err());
251        assert!(result.unwrap_err().contains("Invalid reality_level"));
252    }
253
254    #[test]
255    fn test_find_service_by_path() {
256        let federation = create_test_federation();
257
258        assert!(federation.find_service_by_path("/auth").is_some());
259        assert!(federation.find_service_by_path("/payments").is_some());
260        assert!(federation.find_service_by_path("/unknown").is_none());
261    }
262
263    #[test]
264    fn test_find_service_by_path_nested() {
265        let federation = create_test_federation();
266
267        let service = federation.find_service_by_path("/auth/login").unwrap();
268        assert_eq!(service.name, "auth");
269
270        let service = federation.find_service_by_path("/payments/process").unwrap();
271        assert_eq!(service.name, "payments");
272    }
273
274    #[test]
275    fn test_find_service_by_path_longest_match() {
276        let mut federation = create_test_federation();
277        federation.services.push(ServiceBoundary::new(
278            "auth-admin".to_string(),
279            Uuid::new_v4(),
280            "/auth/admin".to_string(),
281            ServiceRealityLevel::MockV3,
282        ));
283
284        // Should match the longer /auth/admin path
285        let service = federation.find_service_by_path("/auth/admin/users").unwrap();
286        assert_eq!(service.name, "auth-admin");
287
288        // Should match /auth (shorter path)
289        let service = federation.find_service_by_path("/auth/login").unwrap();
290        assert_eq!(service.name, "auth");
291    }
292
293    #[test]
294    fn test_get_service() {
295        let federation = create_test_federation();
296
297        let service = federation.get_service("auth").unwrap();
298        assert_eq!(service.name, "auth");
299
300        let service = federation.get_service("payments").unwrap();
301        assert_eq!(service.name, "payments");
302
303        assert!(federation.get_service("nonexistent").is_none());
304    }
305
306    #[test]
307    fn test_add_service() {
308        let mut federation = create_test_federation();
309        let initial_count = federation.services.len();
310
311        federation.add_service(ServiceBoundary::new(
312            "inventory".to_string(),
313            Uuid::new_v4(),
314            "/inventory".to_string(),
315            ServiceRealityLevel::Blended,
316        ));
317
318        assert_eq!(federation.services.len(), initial_count + 1);
319        assert!(federation.get_service("inventory").is_some());
320    }
321
322    #[test]
323    fn test_add_service_updates_timestamp() {
324        let mut federation = create_test_federation();
325        let original_updated = federation.updated_at;
326
327        // Small delay to ensure timestamp changes
328        std::thread::sleep(std::time::Duration::from_millis(10));
329
330        federation.add_service(ServiceBoundary::new(
331            "new".to_string(),
332            Uuid::new_v4(),
333            "/new".to_string(),
334            ServiceRealityLevel::Real,
335        ));
336
337        assert!(federation.updated_at > original_updated);
338    }
339
340    #[test]
341    fn test_remove_service() {
342        let mut federation = create_test_federation();
343        let initial_count = federation.services.len();
344
345        assert!(federation.remove_service("auth"));
346        assert_eq!(federation.services.len(), initial_count - 1);
347        assert!(federation.get_service("auth").is_none());
348    }
349
350    #[test]
351    fn test_remove_service_not_found() {
352        let mut federation = create_test_federation();
353        let initial_count = federation.services.len();
354
355        assert!(!federation.remove_service("nonexistent"));
356        assert_eq!(federation.services.len(), initial_count);
357    }
358
359    #[test]
360    fn test_remove_service_updates_timestamp() {
361        let mut federation = create_test_federation();
362        let original_updated = federation.updated_at;
363
364        std::thread::sleep(std::time::Duration::from_millis(10));
365
366        federation.remove_service("auth");
367        assert!(federation.updated_at > original_updated);
368    }
369
370    // FederationConfig tests
371    #[test]
372    fn test_federation_config_serialize() {
373        let config = FederationConfig {
374            name: "test".to_string(),
375            description: "Test federation".to_string(),
376            services: vec![],
377        };
378
379        let json = serde_json::to_string(&config).unwrap();
380        assert!(json.contains("\"name\":\"test\""));
381        assert!(json.contains("\"description\":\"Test federation\""));
382    }
383
384    #[test]
385    fn test_federation_config_deserialize() {
386        let json = r#"{"name":"test","description":"","services":[]}"#;
387        let config: FederationConfig = serde_json::from_str(json).unwrap();
388        assert_eq!(config.name, "test");
389        assert!(config.services.is_empty());
390    }
391
392    // FederationService tests
393    #[test]
394    fn test_federation_service_debug() {
395        let service = FederationService {
396            name: "auth".to_string(),
397            workspace_id: Uuid::new_v4().to_string(),
398            base_path: "/auth".to_string(),
399            reality_level: "real".to_string(),
400            config: HashMap::new(),
401            dependencies: Vec::new(),
402        };
403
404        let debug = format!("{:?}", service);
405        assert!(debug.contains("auth"));
406    }
407
408    // Federation serialization tests
409    #[test]
410    fn test_federation_serialize() {
411        let federation = create_test_federation();
412        let json = serde_json::to_string(&federation).unwrap();
413        assert!(json.contains("test"));
414        assert!(json.contains("auth"));
415        assert!(json.contains("payments"));
416    }
417
418    #[test]
419    fn test_federation_clone() {
420        let federation = create_test_federation();
421        let cloned = federation.clone();
422
423        assert_eq!(federation.id, cloned.id);
424        assert_eq!(federation.name, cloned.name);
425        assert_eq!(federation.services.len(), cloned.services.len());
426    }
427}