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