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: service.clone(),
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        let _real = ServiceRealityLevel::Real;
247        let _mock = ServiceRealityLevel::MockV3;
248        let _blended = ServiceRealityLevel::Blended;
249        let _chaos = ServiceRealityLevel::ChaosDriven;
250
251        // Test conversion
252        assert_eq!(ServiceRealityLevel::Real.as_str(), "real");
253        assert_eq!(ServiceRealityLevel::MockV3.as_str(), "mock_v3");
254        assert_eq!(ServiceRealityLevel::Blended.as_str(), "blended");
255        assert_eq!(ServiceRealityLevel::ChaosDriven.as_str(), "chaos_driven");
256    }
257
258    #[test]
259    fn test_federation_mutation_methods() {
260        // Test that Federation mutation methods work
261        let org_id = Uuid::new_v4();
262        let config = FederationConfig {
263            name: "mutable-fed".to_string(),
264            description: String::new(),
265            services: vec![FederationService {
266                name: "initial".to_string(),
267                workspace_id: Uuid::new_v4().to_string(),
268                base_path: "/initial".to_string(),
269                reality_level: "real".to_string(),
270                config: HashMap::new(),
271                dependencies: Vec::new(),
272            }],
273        };
274
275        let mut federation = Federation::from_config(org_id, config).unwrap();
276        assert_eq!(federation.services.len(), 1);
277
278        // Add a service
279        federation.add_service(ServiceBoundary::new(
280            "added".to_string(),
281            Uuid::new_v4(),
282            "/added".to_string(),
283            ServiceRealityLevel::MockV3,
284        ));
285        assert_eq!(federation.services.len(), 2);
286
287        // Remove a service
288        let removed = federation.remove_service("initial");
289        assert!(removed);
290        assert_eq!(federation.services.len(), 1);
291        assert_eq!(federation.services[0].name, "added");
292    }
293
294    #[test]
295    fn test_service_path_extraction() {
296        // Test that service path extraction works through public API
297        let service = ServiceBoundary::new(
298            "api".to_string(),
299            Uuid::new_v4(),
300            "/api/v1".to_string(),
301            ServiceRealityLevel::Real,
302        );
303
304        assert_eq!(service.extract_service_path("/api/v1/users"), Some("/users".to_string()));
305        assert_eq!(service.extract_service_path("/api/v1"), Some("/".to_string()));
306        assert_eq!(service.extract_service_path("/other"), None);
307    }
308
309    #[test]
310    fn test_federation_service_lookup() {
311        // Test finding services in federation
312        let org_id = Uuid::new_v4();
313        let config = FederationConfig {
314            name: "lookup-test".to_string(),
315            description: String::new(),
316            services: vec![
317                FederationService {
318                    name: "service-a".to_string(),
319                    workspace_id: Uuid::new_v4().to_string(),
320                    base_path: "/a".to_string(),
321                    reality_level: "real".to_string(),
322                    config: HashMap::new(),
323                    dependencies: Vec::new(),
324                },
325                FederationService {
326                    name: "service-b".to_string(),
327                    workspace_id: Uuid::new_v4().to_string(),
328                    base_path: "/b".to_string(),
329                    reality_level: "mock_v3".to_string(),
330                    config: HashMap::new(),
331                    dependencies: Vec::new(),
332                },
333            ],
334        };
335
336        let federation = Federation::from_config(org_id, config).unwrap();
337
338        // Get by name
339        let service_a = federation.get_service("service-a");
340        assert!(service_a.is_some());
341        assert_eq!(service_a.unwrap().base_path, "/a");
342
343        // Find by path
344        let service_b = federation.find_service_by_path("/b/endpoint");
345        assert!(service_b.is_some());
346        assert_eq!(service_b.unwrap().name, "service-b");
347    }
348
349    #[test]
350    fn test_complete_workflow() {
351        // Integration test of a complete workflow
352        let org_id = Uuid::new_v4();
353
354        // 1. Create configuration
355        let config = FederationConfig {
356            name: "e-commerce".to_string(),
357            description: "E-commerce platform federation".to_string(),
358            services: vec![
359                FederationService {
360                    name: "auth".to_string(),
361                    workspace_id: Uuid::new_v4().to_string(),
362                    base_path: "/auth".to_string(),
363                    reality_level: "real".to_string(),
364                    config: HashMap::new(),
365                    dependencies: Vec::new(),
366                },
367                FederationService {
368                    name: "catalog".to_string(),
369                    workspace_id: Uuid::new_v4().to_string(),
370                    base_path: "/catalog".to_string(),
371                    reality_level: "mock_v3".to_string(),
372                    config: HashMap::new(),
373                    dependencies: vec!["auth".to_string()],
374                },
375            ],
376        };
377
378        // 2. Create federation from config
379        let federation = Federation::from_config(org_id, config).unwrap();
380        assert_eq!(federation.services.len(), 2);
381
382        // 3. Create router
383        let router = FederationRouter::new(Arc::new(federation));
384
385        // 4. Route requests
386        let auth_route = router.route("/auth/login").unwrap();
387        assert_eq!(auth_route.service.name, "auth");
388        assert_eq!(auth_route.service.reality_level, ServiceRealityLevel::Real);
389
390        let catalog_route = router.route("/catalog/products/123").unwrap();
391        assert_eq!(catalog_route.service.name, "catalog");
392        assert_eq!(catalog_route.service_path, "/products/123");
393        assert_eq!(catalog_route.service.reality_level, ServiceRealityLevel::MockV3);
394
395        // 5. Verify service dependencies
396        let catalog_service = router.services().iter().find(|s| s.name == "catalog").unwrap();
397        assert_eq!(catalog_service.dependencies, vec!["auth".to_string()]);
398    }
399
400    #[test]
401    fn test_yaml_config_roundtrip() {
402        // Test that config can be serialized/deserialized with YAML
403        let config = FederationConfig {
404            name: "yaml-test".to_string(),
405            description: "Testing YAML".to_string(),
406            services: vec![FederationService {
407                name: "test".to_string(),
408                workspace_id: Uuid::new_v4().to_string(),
409                base_path: "/test".to_string(),
410                reality_level: "real".to_string(),
411                config: HashMap::new(),
412                dependencies: Vec::new(),
413            }],
414        };
415
416        let yaml = serde_yaml::to_string(&config).unwrap();
417        let deserialized: FederationConfig = serde_yaml::from_str(&yaml).unwrap();
418
419        assert_eq!(deserialized.name, config.name);
420        assert_eq!(deserialized.description, config.description);
421        assert_eq!(deserialized.services.len(), config.services.len());
422    }
423
424    #[test]
425    fn test_json_config_roundtrip() {
426        // Test that config can be serialized/deserialized with JSON
427        let mut service_config = HashMap::new();
428        service_config.insert("key".to_string(), serde_json::json!("value"));
429
430        let config = FederationConfig {
431            name: "json-test".to_string(),
432            description: "Testing JSON".to_string(),
433            services: vec![FederationService {
434                name: "test".to_string(),
435                workspace_id: Uuid::new_v4().to_string(),
436                base_path: "/test".to_string(),
437                reality_level: "blended".to_string(),
438                config: service_config,
439                dependencies: vec!["dep1".to_string()],
440            }],
441        };
442
443        let json = serde_json::to_string(&config).unwrap();
444        let deserialized: FederationConfig = serde_json::from_str(&json).unwrap();
445
446        assert_eq!(deserialized.name, config.name);
447        assert_eq!(deserialized.services[0].config.len(), 1);
448        assert_eq!(deserialized.services[0].dependencies.len(), 1);
449    }
450}