1use crate::request_logger::RequestLogEntry;
28use regex::Regex;
29use serde::{Deserialize, Serialize};
30use std::collections::HashMap;
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct VerificationRequest {
35 pub method: Option<String>,
38
39 pub path: Option<String>,
42
43 pub query_params: HashMap<String, String>,
46
47 pub headers: HashMap<String, String>,
50
51 pub body_pattern: Option<String>,
54}
55
56impl Default for VerificationRequest {
57 fn default() -> Self {
58 Self {
59 method: None,
60 path: None,
61 query_params: HashMap::new(),
62 headers: HashMap::new(),
63 body_pattern: None,
64 }
65 }
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
70#[serde(tag = "type", rename_all = "snake_case")]
71pub enum VerificationCount {
72 Exactly(usize),
74 AtLeast(usize),
76 AtMost(usize),
78 Never,
80 AtLeastOnce,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct VerificationResult {
87 pub matched: bool,
89 pub count: usize,
91 pub expected: VerificationCount,
93 pub matches: Vec<RequestLogEntry>,
95 pub error_message: Option<String>,
97}
98
99impl VerificationResult {
100 pub fn success(
102 count: usize,
103 expected: VerificationCount,
104 matches: Vec<RequestLogEntry>,
105 ) -> Self {
106 Self {
107 matched: true,
108 count,
109 expected,
110 matches,
111 error_message: None,
112 }
113 }
114
115 pub fn failure(
117 count: usize,
118 expected: VerificationCount,
119 matches: Vec<RequestLogEntry>,
120 error_message: String,
121 ) -> Self {
122 Self {
123 matched: false,
124 count,
125 expected,
126 matches,
127 error_message: Some(error_message),
128 }
129 }
130}
131
132pub fn matches_verification_pattern(
134 entry: &RequestLogEntry,
135 pattern: &VerificationRequest,
136) -> bool {
137 if let Some(ref expected_method) = pattern.method {
139 if entry.method.to_uppercase() != expected_method.to_uppercase() {
140 return false;
141 }
142 }
143
144 if let Some(ref expected_path) = pattern.path {
146 if !matches_path_pattern(&entry.path, expected_path) {
147 return false;
148 }
149 }
150
151 if !pattern.query_params.is_empty() {
157 }
163
164 for (key, expected_value) in &pattern.headers {
166 let header_key_lower = key.to_lowercase();
167 let found = entry
168 .headers
169 .iter()
170 .any(|(k, v)| k.to_lowercase() == header_key_lower && v == expected_value);
171
172 if !found {
173 return false;
174 }
175 }
176
177 if let Some(ref body_pattern) = pattern.body_pattern {
182 if let Some(body_str) = entry.metadata.get("request_body") {
184 if !matches_body_pattern(body_str, body_pattern) {
185 return false;
186 }
187 } else {
188 }
192 }
193
194 true
195}
196
197fn matches_path_pattern(path: &str, pattern: &str) -> bool {
199 if pattern == path {
201 return true;
202 }
203
204 if pattern == "*" {
206 return true;
207 }
208
209 if let Ok(re) = Regex::new(pattern) {
211 if re.is_match(path) {
212 return true;
213 }
214 }
215
216 if pattern.contains('*') {
218 return matches_wildcard_pattern(path, pattern);
219 }
220
221 false
222}
223
224fn matches_wildcard_pattern(path: &str, pattern: &str) -> bool {
226 let pattern_parts: Vec<&str> = pattern.split('/').filter(|s| !s.is_empty()).collect();
227 let path_parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
228
229 match_wildcard_segments(&pattern_parts, &path_parts, 0, 0)
230}
231
232fn match_wildcard_segments(
234 pattern_parts: &[&str],
235 path_parts: &[&str],
236 pattern_idx: usize,
237 path_idx: usize,
238) -> bool {
239 if pattern_idx == pattern_parts.len() && path_idx == path_parts.len() {
241 return true;
242 }
243
244 if pattern_idx == pattern_parts.len() {
246 return false;
247 }
248
249 let current_pattern = pattern_parts[pattern_idx];
250
251 match current_pattern {
252 "*" => {
253 if path_idx < path_parts.len() {
255 if match_wildcard_segments(pattern_parts, path_parts, pattern_idx + 1, path_idx + 1)
257 {
258 return true;
259 }
260 }
261 false
262 }
263 "**" => {
264 if match_wildcard_segments(pattern_parts, path_parts, pattern_idx + 1, path_idx) {
267 return true;
268 }
269 if path_idx < path_parts.len() {
271 if match_wildcard_segments(pattern_parts, path_parts, pattern_idx, path_idx + 1) {
272 return true;
273 }
274 }
275 false
276 }
277 _ => {
278 if path_idx < path_parts.len() && path_parts[path_idx] == current_pattern {
280 match_wildcard_segments(pattern_parts, path_parts, pattern_idx + 1, path_idx + 1)
281 } else {
282 false
283 }
284 }
285 }
286}
287
288fn matches_body_pattern(body: &str, pattern: &str) -> bool {
290 if let Ok(re) = Regex::new(pattern) {
292 re.is_match(body)
293 } else {
294 body == pattern
296 }
297}
298
299pub async fn verify_requests(
301 logger: &crate::request_logger::CentralizedRequestLogger,
302 pattern: &VerificationRequest,
303 expected: VerificationCount,
304) -> VerificationResult {
305 let logs = logger.get_recent_logs(None).await;
307
308 let matches: Vec<RequestLogEntry> = logs
310 .into_iter()
311 .filter(|entry| matches_verification_pattern(entry, pattern))
312 .collect();
313
314 let count = matches.len();
315
316 let matched = match &expected {
318 VerificationCount::Exactly(n) => count == *n,
319 VerificationCount::AtLeast(n) => count >= *n,
320 VerificationCount::AtMost(n) => count <= *n,
321 VerificationCount::Never => count == 0,
322 VerificationCount::AtLeastOnce => count >= 1,
323 };
324
325 if matched {
326 VerificationResult::success(count, expected, matches)
327 } else {
328 let error_message = format!(
329 "Verification failed: expected {:?}, but found {} matching requests",
330 expected, count
331 );
332 VerificationResult::failure(count, expected, matches, error_message)
333 }
334}
335
336pub async fn verify_never(
338 logger: &crate::request_logger::CentralizedRequestLogger,
339 pattern: &VerificationRequest,
340) -> VerificationResult {
341 verify_requests(logger, pattern, VerificationCount::Never).await
342}
343
344pub async fn verify_at_least(
346 logger: &crate::request_logger::CentralizedRequestLogger,
347 pattern: &VerificationRequest,
348 min: usize,
349) -> VerificationResult {
350 verify_requests(logger, pattern, VerificationCount::AtLeast(min)).await
351}
352
353pub async fn verify_sequence(
355 logger: &crate::request_logger::CentralizedRequestLogger,
356 patterns: &[VerificationRequest],
357) -> VerificationResult {
358 let logs = logger.get_recent_logs(None).await;
360
361 let mut log_idx = 0;
363 let mut all_matches = Vec::new();
364
365 for pattern in patterns {
366 let mut found = false;
368 while log_idx < logs.len() {
369 if matches_verification_pattern(&logs[log_idx], pattern) {
370 all_matches.push(logs[log_idx].clone());
371 log_idx += 1;
372 found = true;
373 break;
374 }
375 log_idx += 1;
376 }
377
378 if !found {
379 let error_message = format!(
380 "Sequence verification failed: pattern {:?} not found in sequence",
381 pattern
382 );
383 return VerificationResult::failure(
384 all_matches.len(),
385 VerificationCount::Exactly(patterns.len()),
386 all_matches,
387 error_message,
388 );
389 }
390 }
391
392 VerificationResult::success(
393 all_matches.len(),
394 VerificationCount::Exactly(patterns.len()),
395 all_matches,
396 )
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use crate::request_logger::{create_http_log_entry, CentralizedRequestLogger};
403 use chrono::Utc;
404 use std::collections::HashMap;
405
406 fn create_test_entry(method: &str, path: &str) -> RequestLogEntry {
407 create_http_log_entry(
408 method,
409 path,
410 200,
411 100,
412 Some("127.0.0.1".to_string()),
413 Some("test-agent".to_string()),
414 HashMap::new(),
415 1024,
416 None,
417 )
418 }
419
420 #[tokio::test]
421 async fn test_verify_exactly() {
422 let logger = CentralizedRequestLogger::new(100);
423 logger.log_request(create_test_entry("GET", "/api/users")).await;
424 logger.log_request(create_test_entry("GET", "/api/users")).await;
425 logger.log_request(create_test_entry("GET", "/api/users")).await;
426
427 let pattern = VerificationRequest {
428 method: Some("GET".to_string()),
429 path: Some("/api/users".to_string()),
430 query_params: HashMap::new(),
431 headers: HashMap::new(),
432 body_pattern: None,
433 };
434
435 let result = verify_requests(&logger, &pattern, VerificationCount::Exactly(3)).await;
436 assert!(result.matched);
437 assert_eq!(result.count, 3);
438 }
439
440 #[tokio::test]
441 async fn test_verify_at_least() {
442 let logger = CentralizedRequestLogger::new(100);
443 logger.log_request(create_test_entry("POST", "/api/orders")).await;
444 logger.log_request(create_test_entry("POST", "/api/orders")).await;
445
446 let pattern = VerificationRequest {
447 method: Some("POST".to_string()),
448 path: Some("/api/orders".to_string()),
449 query_params: HashMap::new(),
450 headers: HashMap::new(),
451 body_pattern: None,
452 };
453
454 let result = verify_at_least(&logger, &pattern, 2).await;
455 assert!(result.matched);
456 assert_eq!(result.count, 2);
457
458 let result2 = verify_at_least(&logger, &pattern, 1).await;
459 assert!(result2.matched);
460
461 let result3 = verify_at_least(&logger, &pattern, 3).await;
462 assert!(!result3.matched);
463 }
464
465 #[tokio::test]
466 async fn test_verify_never() {
467 let logger = CentralizedRequestLogger::new(100);
468 logger.log_request(create_test_entry("GET", "/api/users")).await;
469
470 let pattern = VerificationRequest {
471 method: Some("DELETE".to_string()),
472 path: Some("/api/users".to_string()),
473 query_params: HashMap::new(),
474 headers: HashMap::new(),
475 body_pattern: None,
476 };
477
478 let result = verify_never(&logger, &pattern).await;
479 assert!(result.matched);
480 assert_eq!(result.count, 0);
481 }
482
483 #[tokio::test]
484 async fn test_verify_sequence() {
485 let logger = CentralizedRequestLogger::new(100);
486 logger.log_request(create_test_entry("POST", "/api/users")).await;
487 logger.log_request(create_test_entry("GET", "/api/users/1")).await;
488 logger.log_request(create_test_entry("PUT", "/api/users/1")).await;
489
490 let patterns = vec![
491 VerificationRequest {
492 method: Some("POST".to_string()),
493 path: Some("/api/users".to_string()),
494 query_params: HashMap::new(),
495 headers: HashMap::new(),
496 body_pattern: None,
497 },
498 VerificationRequest {
499 method: Some("GET".to_string()),
500 path: Some("/api/users/1".to_string()),
501 query_params: HashMap::new(),
502 headers: HashMap::new(),
503 body_pattern: None,
504 },
505 ];
506
507 let result = verify_sequence(&logger, &patterns).await;
508 assert!(result.matched);
509 assert_eq!(result.count, 2);
510 }
511
512 #[test]
513 fn test_matches_path_pattern_exact() {
514 assert!(matches_path_pattern("/api/users", "/api/users"));
515 assert!(!matches_path_pattern("/api/users", "/api/posts"));
516 }
517
518 #[test]
519 fn test_matches_path_pattern_wildcard() {
520 assert!(matches_path_pattern("/api/users/1", "/api/users/*"));
521 assert!(matches_path_pattern("/api/users/123", "/api/users/*"));
522 assert!(!matches_path_pattern("/api/users/1/posts", "/api/users/*"));
523 }
524
525 #[test]
526 fn test_matches_path_pattern_double_wildcard() {
527 assert!(matches_path_pattern("/api/users/1", "/api/**"));
528 assert!(matches_path_pattern("/api/users/1/posts", "/api/**"));
529 assert!(matches_path_pattern("/api/users", "/api/**"));
530 }
531
532 #[test]
533 fn test_matches_path_pattern_regex() {
534 assert!(matches_path_pattern("/api/users/123", r"^/api/users/\d+$"));
535 assert!(!matches_path_pattern("/api/users/abc", r"^/api/users/\d+$"));
536 }
537
538 #[test]
539 fn test_matches_verification_pattern_method() {
540 let entry = create_test_entry("GET", "/api/users");
541 let pattern = VerificationRequest {
542 method: Some("GET".to_string()),
543 path: None,
544 query_params: HashMap::new(),
545 headers: HashMap::new(),
546 body_pattern: None,
547 };
548 assert!(matches_verification_pattern(&entry, &pattern));
549
550 let pattern2 = VerificationRequest {
551 method: Some("POST".to_string()),
552 path: None,
553 query_params: HashMap::new(),
554 headers: HashMap::new(),
555 body_pattern: None,
556 };
557 assert!(!matches_verification_pattern(&entry, &pattern2));
558 }
559
560 #[test]
561 fn test_matches_verification_pattern_path() {
562 let entry = create_test_entry("GET", "/api/users");
563 let pattern = VerificationRequest {
564 method: None,
565 path: Some("/api/users".to_string()),
566 query_params: HashMap::new(),
567 headers: HashMap::new(),
568 body_pattern: None,
569 };
570 assert!(matches_verification_pattern(&entry, &pattern));
571
572 let pattern2 = VerificationRequest {
573 method: None,
574 path: Some("/api/posts".to_string()),
575 query_params: HashMap::new(),
576 headers: HashMap::new(),
577 body_pattern: None,
578 };
579 assert!(!matches_verification_pattern(&entry, &pattern2));
580 }
581}