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