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 let Ok(re) = Regex::new(pattern) {
197 if re.is_match(path) {
198 return true;
199 }
200 }
201
202 if pattern.contains('*') {
204 return matches_wildcard_pattern(path, pattern);
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 logs = logger.get_recent_logs(None).await;
346
347 let mut log_idx = 0;
349 let mut all_matches = Vec::new();
350
351 for pattern in patterns {
352 let mut found = false;
354 while log_idx < logs.len() {
355 if matches_verification_pattern(&logs[log_idx], pattern) {
356 all_matches.push(logs[log_idx].clone());
357 log_idx += 1;
358 found = true;
359 break;
360 }
361 log_idx += 1;
362 }
363
364 if !found {
365 let error_message = format!(
366 "Sequence verification failed: pattern {:?} not found in sequence",
367 pattern
368 );
369 return VerificationResult::failure(
370 all_matches.len(),
371 VerificationCount::Exactly(patterns.len()),
372 all_matches,
373 error_message,
374 );
375 }
376 }
377
378 VerificationResult::success(
379 all_matches.len(),
380 VerificationCount::Exactly(patterns.len()),
381 all_matches,
382 )
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use crate::request_logger::{create_http_log_entry, CentralizedRequestLogger};
389 use chrono::Utc;
390 use std::collections::HashMap;
391
392 fn create_test_entry(method: &str, path: &str) -> RequestLogEntry {
393 create_http_log_entry(
394 method,
395 path,
396 200,
397 100,
398 Some("127.0.0.1".to_string()),
399 Some("test-agent".to_string()),
400 HashMap::new(),
401 1024,
402 None,
403 )
404 }
405
406 #[tokio::test]
407 async fn test_verify_exactly() {
408 let logger = CentralizedRequestLogger::new(100);
409 logger.log_request(create_test_entry("GET", "/api/users")).await;
410 logger.log_request(create_test_entry("GET", "/api/users")).await;
411 logger.log_request(create_test_entry("GET", "/api/users")).await;
412
413 let pattern = VerificationRequest {
414 method: Some("GET".to_string()),
415 path: Some("/api/users".to_string()),
416 query_params: HashMap::new(),
417 headers: HashMap::new(),
418 body_pattern: None,
419 };
420
421 let result = verify_requests(&logger, &pattern, VerificationCount::Exactly(3)).await;
422 assert!(result.matched);
423 assert_eq!(result.count, 3);
424 }
425
426 #[tokio::test]
427 async fn test_verify_at_least() {
428 let logger = CentralizedRequestLogger::new(100);
429 logger.log_request(create_test_entry("POST", "/api/orders")).await;
430 logger.log_request(create_test_entry("POST", "/api/orders")).await;
431
432 let pattern = VerificationRequest {
433 method: Some("POST".to_string()),
434 path: Some("/api/orders".to_string()),
435 query_params: HashMap::new(),
436 headers: HashMap::new(),
437 body_pattern: None,
438 };
439
440 let result = verify_at_least(&logger, &pattern, 2).await;
441 assert!(result.matched);
442 assert_eq!(result.count, 2);
443
444 let result2 = verify_at_least(&logger, &pattern, 1).await;
445 assert!(result2.matched);
446
447 let result3 = verify_at_least(&logger, &pattern, 3).await;
448 assert!(!result3.matched);
449 }
450
451 #[tokio::test]
452 async fn test_verify_never() {
453 let logger = CentralizedRequestLogger::new(100);
454 logger.log_request(create_test_entry("GET", "/api/users")).await;
455
456 let pattern = VerificationRequest {
457 method: Some("DELETE".to_string()),
458 path: Some("/api/users".to_string()),
459 query_params: HashMap::new(),
460 headers: HashMap::new(),
461 body_pattern: None,
462 };
463
464 let result = verify_never(&logger, &pattern).await;
465 assert!(result.matched);
466 assert_eq!(result.count, 0);
467 }
468
469 #[tokio::test]
470 async fn test_verify_sequence() {
471 let logger = CentralizedRequestLogger::new(100);
472 logger.log_request(create_test_entry("POST", "/api/users")).await;
473 logger.log_request(create_test_entry("GET", "/api/users/1")).await;
474 logger.log_request(create_test_entry("PUT", "/api/users/1")).await;
475
476 let patterns = vec![
477 VerificationRequest {
478 method: Some("POST".to_string()),
479 path: Some("/api/users".to_string()),
480 query_params: HashMap::new(),
481 headers: HashMap::new(),
482 body_pattern: None,
483 },
484 VerificationRequest {
485 method: Some("GET".to_string()),
486 path: Some("/api/users/1".to_string()),
487 query_params: HashMap::new(),
488 headers: HashMap::new(),
489 body_pattern: None,
490 },
491 ];
492
493 let result = verify_sequence(&logger, &patterns).await;
494 assert!(result.matched);
495 assert_eq!(result.count, 2);
496 }
497
498 #[test]
499 fn test_matches_path_pattern_exact() {
500 assert!(matches_path_pattern("/api/users", "/api/users"));
501 assert!(!matches_path_pattern("/api/users", "/api/posts"));
502 }
503
504 #[test]
505 fn test_matches_path_pattern_wildcard() {
506 assert!(matches_path_pattern("/api/users/1", "/api/users/*"));
507 assert!(matches_path_pattern("/api/users/123", "/api/users/*"));
508 assert!(!matches_path_pattern("/api/users/1/posts", "/api/users/*"));
509 }
510
511 #[test]
512 fn test_matches_path_pattern_double_wildcard() {
513 assert!(matches_path_pattern("/api/users/1", "/api/**"));
514 assert!(matches_path_pattern("/api/users/1/posts", "/api/**"));
515 assert!(matches_path_pattern("/api/users", "/api/**"));
516 }
517
518 #[test]
519 fn test_matches_path_pattern_regex() {
520 assert!(matches_path_pattern("/api/users/123", r"^/api/users/\d+$"));
521 assert!(!matches_path_pattern("/api/users/abc", r"^/api/users/\d+$"));
522 }
523
524 #[test]
525 fn test_matches_verification_pattern_method() {
526 let entry = create_test_entry("GET", "/api/users");
527 let pattern = VerificationRequest {
528 method: Some("GET".to_string()),
529 path: None,
530 query_params: HashMap::new(),
531 headers: HashMap::new(),
532 body_pattern: None,
533 };
534 assert!(matches_verification_pattern(&entry, &pattern));
535
536 let pattern2 = VerificationRequest {
537 method: Some("POST".to_string()),
538 path: None,
539 query_params: HashMap::new(),
540 headers: HashMap::new(),
541 body_pattern: None,
542 };
543 assert!(!matches_verification_pattern(&entry, &pattern2));
544 }
545
546 #[test]
547 fn test_matches_verification_pattern_path() {
548 let entry = create_test_entry("GET", "/api/users");
549 let pattern = VerificationRequest {
550 method: None,
551 path: Some("/api/users".to_string()),
552 query_params: HashMap::new(),
553 headers: HashMap::new(),
554 body_pattern: None,
555 };
556 assert!(matches_verification_pattern(&entry, &pattern));
557
558 let pattern2 = VerificationRequest {
559 method: None,
560 path: Some("/api/posts".to_string()),
561 query_params: HashMap::new(),
562 headers: HashMap::new(),
563 body_pattern: None,
564 };
565 assert!(!matches_verification_pattern(&entry, &pattern2));
566 }
567}