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