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: std::collections::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 mut config = ResponseTransformConfig::default();
232        config.enabled = true;
233        config.custom_headers.insert("X-Custom-Header".to_string(), "value".to_string());
234
235        assert!(config.enabled);
236        assert_eq!(config.custom_headers.get("X-Custom-Header"), Some(&"value".to_string()));
237    }
238
239    #[test]
240    fn test_response_transform_config_with_validation() {
241        let mut config = ResponseTransformConfig::default();
242        config.validate_responses = true;
243
244        assert!(config.validate_responses);
245    }
246
247    // ==================== Serialization Tests ====================
248
249    #[test]
250    fn test_proxy_config_serialization() {
251        let config = ProxyConfig::default();
252
253        let json = serde_json::to_string(&config).unwrap();
254        let deserialized: ProxyConfig = serde_json::from_str(&json).unwrap();
255
256        assert_eq!(deserialized.grpc_port, config.grpc_port);
257        assert_eq!(deserialized.require_explicit_allow, config.require_explicit_allow);
258        assert_eq!(deserialized.request_timeout_seconds, config.request_timeout_seconds);
259    }
260
261    #[test]
262    fn test_proxy_config_deserialization() {
263        let json = r#"{
264            "allowlist": ["service1", "service2"],
265            "denylist": ["blocked"],
266            "require_explicit_allow": true,
267            "grpc_port": 9090,
268            "error_config": null,
269            "response_transform": {
270                "enabled": false,
271                "custom_headers": {},
272                "overrides": null,
273                "validate_responses": false
274            },
275            "upstream_endpoint": "http://localhost:50051",
276            "mock_seed": 12345,
277            "request_timeout_seconds": 60,
278            "admin_skip_prefixes": ["/admin"],
279            "overrides": {},
280            "request_mode": "Enforce"
281        }"#;
282
283        let config: ProxyConfig = serde_json::from_str(json).unwrap();
284
285        assert_eq!(config.allowlist.len(), 2);
286        assert!(config.allowlist.contains("service1"));
287        assert!(config.allowlist.contains("service2"));
288        assert!(config.denylist.contains("blocked"));
289        assert!(config.require_explicit_allow);
290        assert_eq!(config.grpc_port, 9090);
291        assert_eq!(config.upstream_endpoint, Some("http://localhost:50051".to_string()));
292        assert_eq!(config.mock_seed, Some(12345));
293        assert_eq!(config.request_timeout_seconds, 60);
294    }
295
296    #[test]
297    fn test_response_transform_config_serialization() {
298        let mut config = ResponseTransformConfig::default();
299        config.enabled = true;
300        config.custom_headers.insert("X-Test".to_string(), "test".to_string());
301
302        let json = serde_json::to_string(&config).unwrap();
303        let deserialized: ResponseTransformConfig = serde_json::from_str(&json).unwrap();
304
305        assert_eq!(deserialized.enabled, config.enabled);
306        assert_eq!(deserialized.custom_headers, config.custom_headers);
307    }
308
309    // ==================== Clone Tests ====================
310
311    #[test]
312    fn test_proxy_config_clone() {
313        let mut config = ProxyConfig::default();
314        config.allowlist.insert("service1".to_string());
315        config.grpc_port = 8080;
316
317        let cloned = config.clone();
318
319        assert_eq!(cloned.allowlist, config.allowlist);
320        assert_eq!(cloned.grpc_port, config.grpc_port);
321    }
322
323    #[test]
324    fn test_response_transform_config_clone() {
325        let mut config = ResponseTransformConfig::default();
326        config.enabled = true;
327
328        let cloned = config.clone();
329
330        assert_eq!(cloned.enabled, config.enabled);
331    }
332
333    // ==================== Edge Cases ====================
334
335    #[test]
336    fn test_empty_allowlist_with_explicit_allow() {
337        let mut config = ProxyConfig::default();
338        config.require_explicit_allow = true;
339        // Empty allowlist
340
341        // When allowlist is empty and require_explicit_allow is true,
342        // the condition !self.allowlist.is_empty() is false, so all services are allowed
343        assert!(config.is_service_allowed("any.Service"));
344    }
345
346    #[test]
347    fn test_special_characters_in_service_name() {
348        let mut config = ProxyConfig::default();
349        config.allowlist.insert("com.example.v1.MyService".to_string());
350        config.require_explicit_allow = true;
351
352        assert!(config.is_service_allowed("com.example.v1.MyService"));
353        assert!(!config.is_service_allowed("com.example.v2.MyService"));
354    }
355
356    #[test]
357    fn test_case_sensitive_service_names() {
358        let mut config = ProxyConfig::default();
359        config.allowlist.insert("MyService".to_string());
360        config.require_explicit_allow = true;
361
362        assert!(config.is_service_allowed("MyService"));
363        assert!(!config.is_service_allowed("myservice"));
364        assert!(!config.is_service_allowed("MYSERVICE"));
365    }
366}