mockforge_runtime_daemon/
detector.rs1use axum::{extract::Request, http::StatusCode, middleware::Next, response::Response};
7use std::sync::Arc;
8use tokio::sync::RwLock;
9use tracing::{debug, info, warn};
10
11use crate::auto_generator::AutoGenerator;
12use crate::config::RuntimeDaemonConfig;
13
14#[derive(Clone)]
16pub struct NotFoundDetector {
17 config: Arc<RuntimeDaemonConfig>,
19 generator: Arc<RwLock<Option<Arc<AutoGenerator>>>>,
21}
22
23impl NotFoundDetector {
24 pub fn new(config: RuntimeDaemonConfig) -> Self {
26 Self {
27 config: Arc::new(config),
28 generator: Arc::new(RwLock::new(None)),
29 }
30 }
31
32 pub async fn set_generator(&self, generator: Arc<AutoGenerator>) {
34 let mut gen = self.generator.write().await;
35 *gen = Some(generator);
36 }
37
38 fn should_exclude(&self, path: &str) -> bool {
40 self.config.exclude_patterns.iter().any(|pattern| {
41 if pattern.starts_with('/') {
42 path.starts_with(pattern)
43 } else {
44 path.contains(pattern)
45 }
46 })
47 }
48
49 pub async fn detect_and_auto_create(self, request: Request, next: Next) -> Response {
51 let method = request.method().clone();
53 let uri = request.uri().clone();
54 let path = uri.path().to_string();
55
56 let response = next.run(request).await;
58
59 if !self.config.enabled || !self.config.auto_create_on_404 {
61 return response;
62 }
63
64 if response.status() != StatusCode::NOT_FOUND {
66 return response;
67 }
68
69 if self.should_exclude(&path) {
71 debug!("Excluding path from auto-generation: {}", path);
72 return response;
73 }
74
75 info!("Detected 404 for {} {}, triggering auto-generation", method, path);
76
77 let generator = self.generator.read().await;
79 if let Some(ref gen) = *generator {
80 let method_str = method.to_string();
82 let path_str = path.clone();
83 let gen_clone = Arc::clone(gen);
84
85 tokio::spawn(async move {
87 if let Err(e) = gen_clone.generate_mock_from_404(&method_str, &path_str).await {
88 warn!("Failed to auto-generate mock for {} {}: {}", method_str, path_str, e);
89 } else {
90 info!("Successfully auto-generated mock for {} {}", method_str, path_str);
91 }
92 });
93 } else {
94 debug!("Auto-generator not available, skipping mock creation");
95 }
96
97 response
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn test_new_detector() {
107 let config = RuntimeDaemonConfig::default();
108 let detector = NotFoundDetector::new(config);
109 assert!(detector.config.exclude_patterns.contains(&"/health".to_string()));
110 }
111
112 #[test]
113 fn test_detector_clone() {
114 let config = RuntimeDaemonConfig {
115 enabled: true,
116 exclude_patterns: vec!["/test".to_string()],
117 ..Default::default()
118 };
119 let detector = NotFoundDetector::new(config);
120 let cloned = detector.clone();
121 assert!(cloned.config.enabled);
122 assert!(cloned.config.exclude_patterns.contains(&"/test".to_string()));
123 }
124
125 #[tokio::test]
126 async fn test_should_exclude() {
127 let config = RuntimeDaemonConfig {
128 exclude_patterns: vec!["/health".to_string(), "/metrics".to_string()],
129 ..Default::default()
130 };
131 let detector = NotFoundDetector::new(config);
132
133 assert!(detector.should_exclude("/health"));
134 assert!(detector.should_exclude("/health/check"));
135 assert!(detector.should_exclude("/metrics"));
136 assert!(!detector.should_exclude("/api/users"));
137 }
138
139 #[test]
140 fn test_should_exclude_prefix_patterns() {
141 let config = RuntimeDaemonConfig {
142 exclude_patterns: vec!["/internal".to_string(), "/admin".to_string()],
143 ..Default::default()
144 };
145 let detector = NotFoundDetector::new(config);
146
147 assert!(detector.should_exclude("/internal"));
149 assert!(detector.should_exclude("/internal/api"));
150 assert!(detector.should_exclude("/internal/users/123"));
151 assert!(detector.should_exclude("/admin"));
152 assert!(detector.should_exclude("/admin/dashboard"));
153 assert!(!detector.should_exclude("/api/internal"));
154 }
155
156 #[test]
157 fn test_should_exclude_contains_patterns() {
158 let config = RuntimeDaemonConfig {
159 exclude_patterns: vec!["secret".to_string(), "private".to_string()],
160 ..Default::default()
161 };
162 let detector = NotFoundDetector::new(config);
163
164 assert!(detector.should_exclude("/api/secret/data"));
166 assert!(detector.should_exclude("/secret"));
167 assert!(detector.should_exclude("/users/secret/key"));
168 assert!(detector.should_exclude("/private/info"));
169 assert!(detector.should_exclude("/api/private"));
170 assert!(!detector.should_exclude("/api/public/data"));
171 }
172
173 #[test]
174 fn test_should_exclude_mixed_patterns() {
175 let config = RuntimeDaemonConfig {
176 exclude_patterns: vec![
177 "/health".to_string(), "internal".to_string(), ],
180 ..Default::default()
181 };
182 let detector = NotFoundDetector::new(config);
183
184 assert!(detector.should_exclude("/health"));
186 assert!(detector.should_exclude("/health/check"));
187 assert!(!detector.should_exclude("/api/health")); assert!(detector.should_exclude("/internal"));
191 assert!(detector.should_exclude("/api/internal"));
192 assert!(detector.should_exclude("/internal/api"));
193 }
194
195 #[test]
196 fn test_should_exclude_empty_patterns() {
197 let config = RuntimeDaemonConfig {
198 exclude_patterns: vec![],
199 ..Default::default()
200 };
201 let detector = NotFoundDetector::new(config);
202
203 assert!(!detector.should_exclude("/health"));
205 assert!(!detector.should_exclude("/metrics"));
206 assert!(!detector.should_exclude("/api/users"));
207 }
208
209 #[test]
210 fn test_should_exclude_default_patterns() {
211 let config = RuntimeDaemonConfig::default();
212 let detector = NotFoundDetector::new(config);
213
214 assert!(detector.should_exclude("/health"));
216 assert!(detector.should_exclude("/metrics"));
217 assert!(detector.should_exclude("/__mockforge"));
218 assert!(detector.should_exclude("/__mockforge/api/mocks"));
219 assert!(!detector.should_exclude("/api/users"));
220 }
221
222 #[tokio::test]
223 async fn test_set_generator() {
224 let config = RuntimeDaemonConfig::default();
225 let detector = NotFoundDetector::new(config.clone());
226
227 {
229 let gen = detector.generator.read().await;
230 assert!(gen.is_none());
231 }
232
233 let auto_gen = Arc::new(AutoGenerator::new(config, "http://localhost:3000".to_string()));
235 detector.set_generator(auto_gen).await;
236
237 {
239 let gen = detector.generator.read().await;
240 assert!(gen.is_some());
241 }
242 }
243
244 #[test]
245 fn test_detector_config_arc_sharing() {
246 let config = RuntimeDaemonConfig {
247 enabled: true,
248 auto_create_on_404: true,
249 ..Default::default()
250 };
251 let detector = NotFoundDetector::new(config);
252 let cloned = detector.clone();
253
254 assert!(Arc::ptr_eq(&detector.config, &cloned.config));
256 }
257}