Skip to main content

mockforge_federation/
lib.rs

1//! # `MockForge` Federation
2//!
3//! Multi-workspace federation for `MockForge`.
4//!
5//! This crate enables composing multiple mock workspaces into a single federated
6//! "virtual system" for large organizations with microservices architectures.
7//!
8//! ## Overview
9//!
10//! Federation allows you to:
11//!
12//! - Define service boundaries and map services to workspaces
13//! - Compose multiple workspaces into one federated virtual system
14//! - Run system-wide scenarios that span multiple services
15//! - Control reality level per service independently
16//!
17//! ## Example Federation
18//!
19//! ```yaml
20//! federation:
21//!   name: "e-commerce-platform"
22//!   services:
23//!     - name: "auth"
24//!       workspace_id: "workspace-auth-123"
25//!       base_path: "/auth"
26//!       reality_level: "real"  # Use real upstream
27//!
28//!     - name: "payments"
29//!       workspace_id: "workspace-payments-456"
30//!       base_path: "/payments"
31//!       reality_level: "mock_v3"
32//!
33//!     - name: "inventory"
34//!       workspace_id: "workspace-inventory-789"
35//!       base_path: "/inventory"
36//!       reality_level: "blended"  # Mix of mock and real
37//!
38//!     - name: "shipping"
39//!       workspace_id: "workspace-shipping-012"
40//!       base_path: "/shipping"
41//!       reality_level: "chaos_driven"  # Chaos testing mode
42//! ```
43//!
44//! ## Features
45//!
46//! - **Service Registry**: Define services and their workspace mappings
47//! - **Federation Router**: Route requests to appropriate workspace based on service
48//! - **Virtual System Manager**: Compose workspaces into unified system
49//! - **Per-Service Reality Level**: Control reality level independently per service
50//! - **System-Wide Scenarios**: Define scenarios that span multiple services
51
52pub mod database;
53pub mod federation;
54pub mod router;
55pub mod service;
56
57pub use database::FederationDatabase;
58pub use federation::{Federation, FederationConfig, FederationService};
59pub use router::{FederationRouter, RoutingResult};
60pub use service::{ServiceBoundary, ServiceRealityLevel};
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use std::collections::HashMap;
66    use std::sync::Arc;
67    use uuid::Uuid;
68
69    // Integration tests to ensure the public API works as expected
70
71    #[test]
72    fn test_service_reality_level_public_api() {
73        // Test that ServiceRealityLevel enum is accessible and usable
74        let level = ServiceRealityLevel::Real;
75        assert_eq!(level.as_str(), "real");
76
77        let parsed = ServiceRealityLevel::from_str("mock_v3");
78        assert_eq!(parsed, Some(ServiceRealityLevel::MockV3));
79    }
80
81    #[test]
82    fn test_service_boundary_public_api() {
83        // Test that ServiceBoundary can be created and used
84        let workspace_id = Uuid::new_v4();
85        let service = ServiceBoundary::new(
86            "test-service".to_string(),
87            workspace_id,
88            "/api".to_string(),
89            ServiceRealityLevel::Blended,
90        );
91
92        assert_eq!(service.name, "test-service");
93        assert_eq!(service.workspace_id, workspace_id);
94        assert_eq!(service.base_path, "/api");
95        assert_eq!(service.reality_level, ServiceRealityLevel::Blended);
96        assert!(service.matches_path("/api/users"));
97        assert!(!service.matches_path("/other"));
98    }
99
100    #[test]
101    fn test_federation_config_public_api() {
102        // Test that FederationConfig can be created and serialized
103        let config = FederationConfig {
104            name: "test-fed".to_string(),
105            description: "Test federation".to_string(),
106            services: vec![FederationService {
107                name: "service1".to_string(),
108                workspace_id: Uuid::new_v4().to_string(),
109                base_path: "/service1".to_string(),
110                reality_level: "real".to_string(),
111                config: HashMap::new(),
112                dependencies: Vec::new(),
113            }],
114        };
115
116        assert_eq!(config.name, "test-fed");
117        assert_eq!(config.services.len(), 1);
118
119        // Test serialization
120        let json = serde_json::to_string(&config).unwrap();
121        assert!(json.contains("test-fed"));
122    }
123
124    #[test]
125    fn test_federation_public_api() {
126        // Test that Federation can be created from config
127        let org_id = Uuid::new_v4();
128        let config = FederationConfig {
129            name: "my-federation".to_string(),
130            description: "My test federation".to_string(),
131            services: vec![FederationService {
132                name: "auth".to_string(),
133                workspace_id: Uuid::new_v4().to_string(),
134                base_path: "/auth".to_string(),
135                reality_level: "real".to_string(),
136                config: HashMap::new(),
137                dependencies: Vec::new(),
138            }],
139        };
140
141        let federation = Federation::from_config(org_id, config).unwrap();
142
143        assert_eq!(federation.name, "my-federation");
144        assert_eq!(federation.org_id, org_id);
145        assert_eq!(federation.services.len(), 1);
146        assert_eq!(federation.services[0].name, "auth");
147    }
148
149    #[test]
150    fn test_federation_router_public_api() {
151        // Test that FederationRouter can route requests
152        let org_id = Uuid::new_v4();
153        let workspace_id = Uuid::new_v4();
154
155        let config = FederationConfig {
156            name: "router-test".to_string(),
157            description: String::new(),
158            services: vec![
159                FederationService {
160                    name: "auth".to_string(),
161                    workspace_id: workspace_id.to_string(),
162                    base_path: "/auth".to_string(),
163                    reality_level: "real".to_string(),
164                    config: HashMap::new(),
165                    dependencies: Vec::new(),
166                },
167                FederationService {
168                    name: "payments".to_string(),
169                    workspace_id: Uuid::new_v4().to_string(),
170                    base_path: "/payments".to_string(),
171                    reality_level: "mock_v3".to_string(),
172                    config: HashMap::new(),
173                    dependencies: Vec::new(),
174                },
175            ],
176        };
177
178        let federation = Federation::from_config(org_id, config).unwrap();
179        let router = FederationRouter::new(Arc::new(federation));
180
181        // Test routing
182        let result = router.route("/auth/login").unwrap();
183        assert_eq!(result.service.name, "auth");
184        assert_eq!(result.workspace_id, workspace_id);
185        assert_eq!(result.service_path, "/login");
186
187        // Test no match
188        assert!(router.route("/unknown").is_none());
189    }
190
191    #[test]
192    fn test_routing_result_public_api() {
193        // Test that RoutingResult is accessible and usable
194        let workspace_id = Uuid::new_v4();
195        let service = Arc::new(ServiceBoundary::new(
196            "test".to_string(),
197            workspace_id,
198            "/test".to_string(),
199            ServiceRealityLevel::Real,
200        ));
201
202        let result = RoutingResult {
203            workspace_id,
204            service,
205            service_path: "/path".to_string(),
206        };
207
208        assert_eq!(result.workspace_id, workspace_id);
209        assert_eq!(result.service.name, "test");
210        assert_eq!(result.service_path, "/path");
211
212        // Test that it can be cloned
213        let cloned = result.clone();
214        assert_eq!(cloned.workspace_id, result.workspace_id);
215    }
216
217    #[test]
218    fn test_federation_service_with_config() {
219        // Test FederationService with complex config
220        let mut config = HashMap::new();
221        config.insert("timeout".to_string(), serde_json::json!(3000));
222        config.insert("retries".to_string(), serde_json::json!(5));
223
224        let service = FederationService {
225            name: "api".to_string(),
226            workspace_id: Uuid::new_v4().to_string(),
227            base_path: "/api".to_string(),
228            reality_level: "blended".to_string(),
229            config: config.clone(),
230            dependencies: vec!["auth".to_string(), "db".to_string()],
231        };
232
233        assert_eq!(service.config.len(), 2);
234        assert_eq!(service.dependencies.len(), 2);
235
236        // Test serialization/deserialization
237        let json = serde_json::to_string(&service).unwrap();
238        let deserialized: FederationService = serde_json::from_str(&json).unwrap();
239        assert_eq!(deserialized.name, service.name);
240        assert_eq!(deserialized.config.len(), service.config.len());
241    }
242
243    #[test]
244    fn test_all_reality_levels_exposed() {
245        // Ensure all reality levels are accessible through public API
246        // Verify all reality levels are constructible
247        assert_eq!(ServiceRealityLevel::Real.as_str(), "real");
248        assert_eq!(ServiceRealityLevel::MockV3.as_str(), "mock_v3");
249        assert_eq!(ServiceRealityLevel::Blended.as_str(), "blended");
250        assert_eq!(ServiceRealityLevel::ChaosDriven.as_str(), "chaos_driven");
251
252        // Test conversion
253        assert_eq!(ServiceRealityLevel::Real.as_str(), "real");
254        assert_eq!(ServiceRealityLevel::MockV3.as_str(), "mock_v3");
255        assert_eq!(ServiceRealityLevel::Blended.as_str(), "blended");
256        assert_eq!(ServiceRealityLevel::ChaosDriven.as_str(), "chaos_driven");
257    }
258
259    #[test]
260    fn test_federation_mutation_methods() {
261        // Test that Federation mutation methods work
262        let org_id = Uuid::new_v4();
263        let config = FederationConfig {
264            name: "mutable-fed".to_string(),
265            description: String::new(),
266            services: vec![FederationService {
267                name: "initial".to_string(),
268                workspace_id: Uuid::new_v4().to_string(),
269                base_path: "/initial".to_string(),
270                reality_level: "real".to_string(),
271                config: HashMap::new(),
272                dependencies: Vec::new(),
273            }],
274        };
275
276        let mut federation = Federation::from_config(org_id, config).unwrap();
277        assert_eq!(federation.services.len(), 1);
278
279        // Add a service
280        federation.add_service(ServiceBoundary::new(
281            "added".to_string(),
282            Uuid::new_v4(),
283            "/added".to_string(),
284            ServiceRealityLevel::MockV3,
285        ));
286        assert_eq!(federation.services.len(), 2);
287
288        // Remove a service
289        let removed = federation.remove_service("initial");
290        assert!(removed);
291        assert_eq!(federation.services.len(), 1);
292        assert_eq!(federation.services[0].name, "added");
293    }
294
295    #[test]
296    fn test_service_path_extraction() {
297        // Test that service path extraction works through public API
298        let service = ServiceBoundary::new(
299            "api".to_string(),
300            Uuid::new_v4(),
301            "/api/v1".to_string(),
302            ServiceRealityLevel::Real,
303        );
304
305        assert_eq!(service.extract_service_path("/api/v1/users"), Some("/users".to_string()));
306        assert_eq!(service.extract_service_path("/api/v1"), Some("/".to_string()));
307        assert_eq!(service.extract_service_path("/other"), None);
308    }
309
310    #[test]
311    fn test_federation_service_lookup() {
312        // Test finding services in federation
313        let org_id = Uuid::new_v4();
314        let config = FederationConfig {
315            name: "lookup-test".to_string(),
316            description: String::new(),
317            services: vec![
318                FederationService {
319                    name: "service-a".to_string(),
320                    workspace_id: Uuid::new_v4().to_string(),
321                    base_path: "/a".to_string(),
322                    reality_level: "real".to_string(),
323                    config: HashMap::new(),
324                    dependencies: Vec::new(),
325                },
326                FederationService {
327                    name: "service-b".to_string(),
328                    workspace_id: Uuid::new_v4().to_string(),
329                    base_path: "/b".to_string(),
330                    reality_level: "mock_v3".to_string(),
331                    config: HashMap::new(),
332                    dependencies: Vec::new(),
333                },
334            ],
335        };
336
337        let federation = Federation::from_config(org_id, config).unwrap();
338
339        // Get by name
340        let service_a = federation.get_service("service-a");
341        assert!(service_a.is_some());
342        assert_eq!(service_a.unwrap().base_path, "/a");
343
344        // Find by path
345        let service_b = federation.find_service_by_path("/b/endpoint");
346        assert!(service_b.is_some());
347        assert_eq!(service_b.unwrap().name, "service-b");
348    }
349
350    #[test]
351    fn test_complete_workflow() {
352        // Integration test of a complete workflow
353        let org_id = Uuid::new_v4();
354
355        // 1. Create configuration
356        let config = FederationConfig {
357            name: "e-commerce".to_string(),
358            description: "E-commerce platform federation".to_string(),
359            services: vec![
360                FederationService {
361                    name: "auth".to_string(),
362                    workspace_id: Uuid::new_v4().to_string(),
363                    base_path: "/auth".to_string(),
364                    reality_level: "real".to_string(),
365                    config: HashMap::new(),
366                    dependencies: Vec::new(),
367                },
368                FederationService {
369                    name: "catalog".to_string(),
370                    workspace_id: Uuid::new_v4().to_string(),
371                    base_path: "/catalog".to_string(),
372                    reality_level: "mock_v3".to_string(),
373                    config: HashMap::new(),
374                    dependencies: vec!["auth".to_string()],
375                },
376            ],
377        };
378
379        // 2. Create federation from config
380        let federation = Federation::from_config(org_id, config).unwrap();
381        assert_eq!(federation.services.len(), 2);
382
383        // 3. Create router
384        let router = FederationRouter::new(Arc::new(federation));
385
386        // 4. Route requests
387        let auth_route = router.route("/auth/login").unwrap();
388        assert_eq!(auth_route.service.name, "auth");
389        assert_eq!(auth_route.service.reality_level, ServiceRealityLevel::Real);
390
391        let catalog_route = router.route("/catalog/products/123").unwrap();
392        assert_eq!(catalog_route.service.name, "catalog");
393        assert_eq!(catalog_route.service_path, "/products/123");
394        assert_eq!(catalog_route.service.reality_level, ServiceRealityLevel::MockV3);
395
396        // 5. Verify service dependencies
397        let catalog_service = router.services().iter().find(|s| s.name == "catalog").unwrap();
398        assert_eq!(catalog_service.dependencies, vec!["auth".to_string()]);
399    }
400
401    #[test]
402    fn test_yaml_config_roundtrip() {
403        // Test that config can be serialized/deserialized with YAML
404        let config = FederationConfig {
405            name: "yaml-test".to_string(),
406            description: "Testing YAML".to_string(),
407            services: vec![FederationService {
408                name: "test".to_string(),
409                workspace_id: Uuid::new_v4().to_string(),
410                base_path: "/test".to_string(),
411                reality_level: "real".to_string(),
412                config: HashMap::new(),
413                dependencies: Vec::new(),
414            }],
415        };
416
417        let yaml = serde_yaml::to_string(&config).unwrap();
418        let deserialized: FederationConfig = serde_yaml::from_str(&yaml).unwrap();
419
420        assert_eq!(deserialized.name, config.name);
421        assert_eq!(deserialized.description, config.description);
422        assert_eq!(deserialized.services.len(), config.services.len());
423    }
424
425    #[test]
426    fn test_json_config_roundtrip() {
427        // Test that config can be serialized/deserialized with JSON
428        let mut service_config = HashMap::new();
429        service_config.insert("key".to_string(), serde_json::json!("value"));
430
431        let config = FederationConfig {
432            name: "json-test".to_string(),
433            description: "Testing JSON".to_string(),
434            services: vec![FederationService {
435                name: "test".to_string(),
436                workspace_id: Uuid::new_v4().to_string(),
437                base_path: "/test".to_string(),
438                reality_level: "blended".to_string(),
439                config: service_config,
440                dependencies: vec!["dep1".to_string()],
441            }],
442        };
443
444        let json = serde_json::to_string(&config).unwrap();
445        let deserialized: FederationConfig = serde_json::from_str(&json).unwrap();
446
447        assert_eq!(deserialized.name, config.name);
448        assert_eq!(deserialized.services[0].config.len(), 1);
449        assert_eq!(deserialized.services[0].dependencies.len(), 1);
450    }
451}