Skip to main content

mockforge_grpc/reflection/
config.rs

1//! Configuration for the reflection proxy
2
3use crate::reflection::error_handling::ErrorConfig;
4use mockforge_core::overrides::Overrides;
5use mockforge_openapi::openapi_routes::ValidationMode;
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet};
8
9/// Configuration for the reflection proxy
10#[derive(Debug, Clone, Deserialize, Serialize)]
11pub struct ProxyConfig {
12    /// List of allowed services (if empty, all services are allowed)
13    pub allowlist: HashSet<String>,
14    /// List of denied services (takes precedence over allowlist)
15    pub denylist: HashSet<String>,
16    /// Whether to require services to be explicitly allowed
17    pub require_explicit_allow: bool,
18    /// gRPC port for connection pooling
19    pub grpc_port: u16,
20    /// Error handling configuration
21    pub error_config: Option<ErrorConfig>,
22    /// Response transformation configuration
23    pub response_transform: ResponseTransformConfig,
24    /// Upstream endpoint for request forwarding
25    pub upstream_endpoint: Option<String>,
26    /// Seed for deterministic mock data generation
27    pub mock_seed: Option<u64>,
28    /// Request timeout in seconds
29    pub request_timeout_seconds: u64,
30    /// Admin skip prefixes
31    pub admin_skip_prefixes: Vec<String>,
32    /// Validation mode overrides
33    pub overrides: HashMap<String, ValidationMode>,
34    /// Default request mode
35    pub request_mode: ValidationMode,
36}
37
38impl Default for ProxyConfig {
39    fn default() -> Self {
40        Self {
41            allowlist: HashSet::new(),
42            denylist: HashSet::new(),
43            require_explicit_allow: false,
44            grpc_port: default_grpc_port(),
45            error_config: None,
46            response_transform: ResponseTransformConfig::default(),
47            upstream_endpoint: None,
48            mock_seed: None,
49            request_timeout_seconds: default_request_timeout_seconds(),
50            admin_skip_prefixes: Vec::new(),
51            overrides: HashMap::new(),
52            request_mode: ValidationMode::default(),
53        }
54    }
55}
56
57/// Default gRPC port
58fn default_grpc_port() -> u16 {
59    50051
60}
61
62/// Default request timeout in seconds
63fn default_request_timeout_seconds() -> u64 {
64    30
65}
66
67/// Configuration for response transformations
68#[derive(Debug, Clone, Deserialize, Serialize, Default)]
69pub struct ResponseTransformConfig {
70    /// Enable response transformations
71    pub enabled: bool,
72    /// Custom headers to add to all responses
73    pub custom_headers: HashMap<String, String>,
74    /// Response body overrides using the override system
75    pub overrides: Option<Overrides>,
76    /// Enable response validation
77    pub validate_responses: bool,
78}
79
80impl ProxyConfig {
81    /// Check if a service is allowed
82    pub fn is_service_allowed(&self, service_name: &str) -> bool {
83        // If service is explicitly denied, it's not allowed
84        if self.denylist.contains(service_name) {
85            return false;
86        }
87
88        // If we require explicit allow and service is not in allowlist, it's not allowed
89        if self.require_explicit_allow
90            && !self.allowlist.is_empty()
91            && !self.allowlist.contains(service_name)
92        {
93            return false;
94        }
95
96        true
97    }
98
99    /// Check if a service is denied
100    pub fn is_service_denied(&self, service_name: &str) -> bool {
101        self.denylist.contains(service_name)
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    // ==================== ProxyConfig Default Tests ====================
110
111    #[test]
112    fn test_proxy_config_default() {
113        let config = ProxyConfig::default();
114
115        assert!(config.allowlist.is_empty());
116        assert!(config.denylist.is_empty());
117        assert!(!config.require_explicit_allow);
118        assert_eq!(config.grpc_port, 50051);
119        assert!(config.error_config.is_none());
120        assert!(config.upstream_endpoint.is_none());
121        assert!(config.mock_seed.is_none());
122        assert_eq!(config.request_timeout_seconds, 30);
123        assert!(config.admin_skip_prefixes.is_empty());
124        assert!(config.overrides.is_empty());
125    }
126
127    #[test]
128    fn test_proxy_config_default_grpc_port() {
129        assert_eq!(default_grpc_port(), 50051);
130    }
131
132    #[test]
133    fn test_proxy_config_default_timeout() {
134        assert_eq!(default_request_timeout_seconds(), 30);
135    }
136
137    // ==================== Service Allowlist Tests ====================
138
139    #[test]
140    fn test_is_service_allowed_empty_lists() {
141        let config = ProxyConfig::default();
142
143        assert!(config.is_service_allowed("any.service.Name"));
144        assert!(config.is_service_allowed("another.Service"));
145    }
146
147    #[test]
148    fn test_is_service_allowed_in_allowlist() {
149        let mut config = ProxyConfig::default();
150        config.allowlist.insert("my.allowed.Service".to_string());
151        config.require_explicit_allow = true;
152
153        assert!(config.is_service_allowed("my.allowed.Service"));
154    }
155
156    #[test]
157    fn test_is_service_allowed_not_in_allowlist_explicit() {
158        let mut config = ProxyConfig::default();
159        config.allowlist.insert("my.allowed.Service".to_string());
160        config.require_explicit_allow = true;
161
162        assert!(!config.is_service_allowed("other.Service"));
163    }
164
165    #[test]
166    fn test_is_service_allowed_not_explicit_mode() {
167        let mut config = ProxyConfig::default();
168        config.allowlist.insert("my.allowed.Service".to_string());
169        config.require_explicit_allow = false;
170
171        // Without explicit allow requirement, all services are allowed
172        assert!(config.is_service_allowed("other.Service"));
173        assert!(config.is_service_allowed("my.allowed.Service"));
174    }
175
176    // ==================== Service Denylist Tests ====================
177
178    #[test]
179    fn test_is_service_denied_empty_denylist() {
180        let config = ProxyConfig::default();
181
182        assert!(!config.is_service_denied("any.service.Name"));
183    }
184
185    #[test]
186    fn test_is_service_denied_in_denylist() {
187        let mut config = ProxyConfig::default();
188        config.denylist.insert("blocked.Service".to_string());
189
190        assert!(config.is_service_denied("blocked.Service"));
191        assert!(!config.is_service_denied("other.Service"));
192    }
193
194    #[test]
195    fn test_denylist_takes_precedence() {
196        let mut config = ProxyConfig::default();
197        config.allowlist.insert("my.Service".to_string());
198        config.denylist.insert("my.Service".to_string());
199        config.require_explicit_allow = true;
200
201        // Service is in both lists, denylist takes precedence
202        assert!(!config.is_service_allowed("my.Service"));
203    }
204
205    #[test]
206    fn test_multiple_services_in_denylist() {
207        let mut config = ProxyConfig::default();
208        config.denylist.insert("blocked1.Service".to_string());
209        config.denylist.insert("blocked2.Service".to_string());
210        config.denylist.insert("blocked3.Service".to_string());
211
212        assert!(config.is_service_denied("blocked1.Service"));
213        assert!(config.is_service_denied("blocked2.Service"));
214        assert!(config.is_service_denied("blocked3.Service"));
215        assert!(!config.is_service_denied("allowed.Service"));
216    }
217
218    // ==================== ResponseTransformConfig Tests ====================
219
220    #[test]
221    fn test_response_transform_config_default() {
222        let config = ResponseTransformConfig::default();
223
224        assert!(!config.enabled);
225        assert!(config.custom_headers.is_empty());
226        assert!(config.overrides.is_none());
227        assert!(!config.validate_responses);
228    }
229
230    #[test]
231    fn test_response_transform_config_with_headers() {
232        let config = ResponseTransformConfig {
233            enabled: true,
234            custom_headers: {
235                let mut headers = HashMap::new();
236                headers.insert("X-Custom-Header".to_string(), "value".to_string());
237                headers
238            },
239            ..Default::default()
240        };
241
242        assert!(config.enabled);
243        assert_eq!(config.custom_headers.get("X-Custom-Header"), Some(&"value".to_string()));
244    }
245
246    #[test]
247    fn test_response_transform_config_with_validation() {
248        let config = ResponseTransformConfig {
249            validate_responses: true,
250            ..Default::default()
251        };
252
253        assert!(config.validate_responses);
254    }
255
256    // ==================== Serialization Tests ====================
257
258    #[test]
259    fn test_proxy_config_serialization() {
260        let config = ProxyConfig::default();
261
262        let json = serde_json::to_string(&config).unwrap();
263        let deserialized: ProxyConfig = serde_json::from_str(&json).unwrap();
264
265        assert_eq!(deserialized.grpc_port, config.grpc_port);
266        assert_eq!(deserialized.require_explicit_allow, config.require_explicit_allow);
267        assert_eq!(deserialized.request_timeout_seconds, config.request_timeout_seconds);
268    }
269
270    #[test]
271    fn test_proxy_config_deserialization() {
272        let json = r#"{
273            "allowlist": ["service1", "service2"],
274            "denylist": ["blocked"],
275            "require_explicit_allow": true,
276            "grpc_port": 9090,
277            "error_config": null,
278            "response_transform": {
279                "enabled": false,
280                "custom_headers": {},
281                "overrides": null,
282                "validate_responses": false
283            },
284            "upstream_endpoint": "http://localhost:50051",
285            "mock_seed": 12345,
286            "request_timeout_seconds": 60,
287            "admin_skip_prefixes": ["/admin"],
288            "overrides": {},
289            "request_mode": "Enforce"
290        }"#;
291
292        let config: ProxyConfig = serde_json::from_str(json).unwrap();
293
294        assert_eq!(config.allowlist.len(), 2);
295        assert!(config.allowlist.contains("service1"));
296        assert!(config.allowlist.contains("service2"));
297        assert!(config.denylist.contains("blocked"));
298        assert!(config.require_explicit_allow);
299        assert_eq!(config.grpc_port, 9090);
300        assert_eq!(config.upstream_endpoint, Some("http://localhost:50051".to_string()));
301        assert_eq!(config.mock_seed, Some(12345));
302        assert_eq!(config.request_timeout_seconds, 60);
303    }
304
305    #[test]
306    fn test_response_transform_config_serialization() {
307        let config = ResponseTransformConfig {
308            enabled: true,
309            custom_headers: {
310                let mut headers = HashMap::new();
311                headers.insert("X-Test".to_string(), "test".to_string());
312                headers
313            },
314            ..Default::default()
315        };
316
317        let json = serde_json::to_string(&config).unwrap();
318        let deserialized: ResponseTransformConfig = serde_json::from_str(&json).unwrap();
319
320        assert_eq!(deserialized.enabled, config.enabled);
321        assert_eq!(deserialized.custom_headers, config.custom_headers);
322    }
323
324    // ==================== Clone Tests ====================
325
326    #[test]
327    fn test_proxy_config_clone() {
328        let mut config = ProxyConfig::default();
329        config.allowlist.insert("service1".to_string());
330        config.grpc_port = 8080;
331
332        let cloned = config.clone();
333
334        assert_eq!(cloned.allowlist, config.allowlist);
335        assert_eq!(cloned.grpc_port, config.grpc_port);
336    }
337
338    #[test]
339    fn test_response_transform_config_clone() {
340        let config = ResponseTransformConfig {
341            enabled: true,
342            ..Default::default()
343        };
344
345        let cloned = config.clone();
346
347        assert_eq!(cloned.enabled, config.enabled);
348    }
349
350    // ==================== Edge Cases ====================
351
352    #[test]
353    fn test_empty_allowlist_with_explicit_allow() {
354        let config = ProxyConfig {
355            require_explicit_allow: true,
356            ..Default::default()
357        };
358        // Empty allowlist
359
360        // When allowlist is empty and require_explicit_allow is true,
361        // the condition !self.allowlist.is_empty() is false, so all services are allowed
362        assert!(config.is_service_allowed("any.Service"));
363    }
364
365    #[test]
366    fn test_special_characters_in_service_name() {
367        let mut config = ProxyConfig::default();
368        config.allowlist.insert("com.example.v1.MyService".to_string());
369        config.require_explicit_allow = true;
370
371        assert!(config.is_service_allowed("com.example.v1.MyService"));
372        assert!(!config.is_service_allowed("com.example.v2.MyService"));
373    }
374
375    #[test]
376    fn test_case_sensitive_service_names() {
377        let mut config = ProxyConfig::default();
378        config.allowlist.insert("MyService".to_string());
379        config.require_explicit_allow = true;
380
381        assert!(config.is_service_allowed("MyService"));
382        assert!(!config.is_service_allowed("myservice"));
383        assert!(!config.is_service_allowed("MYSERVICE"));
384    }
385}