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