Skip to main content

jugar_probar/
network.rs

1//! Network Request Interception (Feature 7)
2//!
3//! Mock API responses and intercept network requests for testing.
4//!
5//! ## EXTREME TDD: Tests written FIRST per spec
6//!
7//! ## Toyota Way Application
8//!
9//! - **Poka-Yoke**: Type-safe route matching prevents invalid patterns
10//! - **Jidoka**: Immediate feedback on unexpected requests
11//! - **Muda**: Only intercept relevant requests
12
13use crate::result::{ProbarError, ProbarResult};
14use serde::{Deserialize, Serialize};
15use std::collections::HashMap;
16use std::sync::{Arc, Mutex};
17
18// =============================================================================
19// PMAT-006: Network Features (Playwright Parity)
20// =============================================================================
21
22/// Reasons for aborting a network request
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub enum AbortReason {
25    /// Request failed
26    Failed,
27    /// Request was aborted
28    Aborted,
29    /// Request timed out
30    TimedOut,
31    /// Access was denied
32    AccessDenied,
33    /// Connection was closed
34    ConnectionClosed,
35    /// Connection failed to establish
36    ConnectionFailed,
37    /// Connection was refused
38    ConnectionRefused,
39    /// Connection was reset
40    ConnectionReset,
41    /// Internet is disconnected
42    InternetDisconnected,
43    /// DNS name could not be resolved
44    NameNotResolved,
45    /// Request was blocked by client
46    BlockedByClient,
47}
48
49impl AbortReason {
50    /// Get the error message for this abort reason
51    #[must_use]
52    pub const fn message(&self) -> &'static str {
53        match self {
54            Self::Failed => "net::ERR_FAILED",
55            Self::Aborted => "net::ERR_ABORTED",
56            Self::TimedOut => "net::ERR_TIMED_OUT",
57            Self::AccessDenied => "net::ERR_ACCESS_DENIED",
58            Self::ConnectionClosed => "net::ERR_CONNECTION_CLOSED",
59            Self::ConnectionFailed => "net::ERR_CONNECTION_FAILED",
60            Self::ConnectionRefused => "net::ERR_CONNECTION_REFUSED",
61            Self::ConnectionReset => "net::ERR_CONNECTION_RESET",
62            Self::InternetDisconnected => "net::ERR_INTERNET_DISCONNECTED",
63            Self::NameNotResolved => "net::ERR_NAME_NOT_RESOLVED",
64            Self::BlockedByClient => "net::ERR_BLOCKED_BY_CLIENT",
65        }
66    }
67}
68
69/// Action to take when a route matches
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub enum RouteAction {
72    /// Respond with a mock response
73    Respond(MockResponse),
74    /// Abort the request
75    Abort(AbortReason),
76    /// Continue the request (let it pass through)
77    Continue,
78}
79
80impl Default for RouteAction {
81    fn default() -> Self {
82        Self::Continue
83    }
84}
85
86/// HTTP methods for request matching
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
88pub enum HttpMethod {
89    /// GET request
90    Get,
91    /// POST request
92    Post,
93    /// PUT request
94    Put,
95    /// DELETE request
96    Delete,
97    /// PATCH request
98    Patch,
99    /// HEAD request
100    Head,
101    /// OPTIONS request
102    Options,
103    /// Any method
104    Any,
105}
106
107impl HttpMethod {
108    /// Parse from string
109    #[must_use]
110    pub fn from_str(s: &str) -> Self {
111        match s.to_uppercase().as_str() {
112            "GET" => Self::Get,
113            "POST" => Self::Post,
114            "PUT" => Self::Put,
115            "DELETE" => Self::Delete,
116            "PATCH" => Self::Patch,
117            "HEAD" => Self::Head,
118            "OPTIONS" => Self::Options,
119            _ => Self::Any,
120        }
121    }
122
123    /// Convert to string
124    #[must_use]
125    pub const fn as_str(&self) -> &'static str {
126        match self {
127            Self::Get => "GET",
128            Self::Post => "POST",
129            Self::Put => "PUT",
130            Self::Delete => "DELETE",
131            Self::Patch => "PATCH",
132            Self::Head => "HEAD",
133            Self::Options => "OPTIONS",
134            Self::Any => "*",
135        }
136    }
137
138    /// Check if this method matches another
139    #[must_use]
140    pub fn matches(&self, other: &Self) -> bool {
141        *self == Self::Any || *other == Self::Any || *self == *other
142    }
143}
144
145/// A mocked HTTP response
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct MockResponse {
148    /// HTTP status code
149    pub status: u16,
150    /// Response headers
151    pub headers: HashMap<String, String>,
152    /// Response body
153    pub body: Vec<u8>,
154    /// Content type
155    pub content_type: String,
156    /// Artificial delay in milliseconds
157    pub delay_ms: u64,
158}
159
160impl Default for MockResponse {
161    fn default() -> Self {
162        Self {
163            status: 200,
164            headers: HashMap::new(),
165            body: Vec::new(),
166            content_type: "application/json".to_string(),
167            delay_ms: 0,
168        }
169    }
170}
171
172impl MockResponse {
173    /// Create a new mock response
174    #[must_use]
175    pub fn new() -> Self {
176        Self::default()
177    }
178
179    /// Create a JSON response
180    #[must_use]
181    pub fn json<T: Serialize>(data: &T) -> ProbarResult<Self> {
182        let body = serde_json::to_vec(data)?;
183        Ok(Self {
184            status: 200,
185            headers: HashMap::new(),
186            body,
187            content_type: "application/json".to_string(),
188            delay_ms: 0,
189        })
190    }
191
192    /// Create a text response
193    #[must_use]
194    pub fn text(content: &str) -> Self {
195        Self {
196            status: 200,
197            headers: HashMap::new(),
198            body: content.as_bytes().to_vec(),
199            content_type: "text/plain".to_string(),
200            delay_ms: 0,
201        }
202    }
203
204    /// Create an error response
205    #[must_use]
206    pub fn error(status: u16, message: &str) -> Self {
207        let body = serde_json::json!({ "error": message }).to_string();
208        Self {
209            status,
210            headers: HashMap::new(),
211            body: body.into_bytes(),
212            content_type: "application/json".to_string(),
213            delay_ms: 0,
214        }
215    }
216
217    /// Set status code
218    #[must_use]
219    pub const fn with_status(mut self, status: u16) -> Self {
220        self.status = status;
221        self
222    }
223
224    /// Set body
225    #[must_use]
226    pub fn with_body(mut self, body: Vec<u8>) -> Self {
227        self.body = body;
228        self
229    }
230
231    /// Set JSON body
232    pub fn with_json<T: Serialize>(mut self, data: &T) -> ProbarResult<Self> {
233        self.body = serde_json::to_vec(data)?;
234        self.content_type = "application/json".to_string();
235        Ok(self)
236    }
237
238    /// Add a header
239    #[must_use]
240    pub fn with_header(mut self, key: &str, value: &str) -> Self {
241        self.headers.insert(key.to_string(), value.to_string());
242        self
243    }
244
245    /// Set content type
246    #[must_use]
247    pub fn with_content_type(mut self, content_type: &str) -> Self {
248        self.content_type = content_type.to_string();
249        self
250    }
251
252    /// Set delay
253    #[must_use]
254    pub const fn with_delay(mut self, delay_ms: u64) -> Self {
255        self.delay_ms = delay_ms;
256        self
257    }
258
259    /// Get body as string
260    #[must_use]
261    pub fn body_string(&self) -> String {
262        String::from_utf8_lossy(&self.body).to_string()
263    }
264}
265
266/// Pattern for matching request URLs
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub enum UrlPattern {
269    /// Exact URL match
270    Exact(String),
271    /// Prefix match
272    Prefix(String),
273    /// Contains substring
274    Contains(String),
275    /// Regex match
276    Regex(String),
277    /// Glob pattern (e.g., "**/api/users/*")
278    Glob(String),
279    /// Match any URL
280    Any,
281}
282
283impl UrlPattern {
284    /// Check if a URL matches this pattern
285    #[must_use]
286    pub fn matches(&self, url: &str) -> bool {
287        match self {
288            Self::Exact(pattern) => url == pattern,
289            Self::Prefix(pattern) => url.starts_with(pattern),
290            Self::Contains(pattern) => url.contains(pattern),
291            Self::Regex(pattern) => regex::Regex::new(pattern)
292                .map(|re| re.is_match(url))
293                .unwrap_or(false),
294            Self::Glob(pattern) => Self::glob_matches(pattern, url),
295            Self::Any => true,
296        }
297    }
298
299    /// Simple glob matching for URLs
300    fn glob_matches(pattern: &str, url: &str) -> bool {
301        let parts: Vec<&str> = pattern.split('*').collect();
302        if parts.is_empty() {
303            return url.is_empty();
304        }
305
306        let mut pos = 0;
307        for (i, part) in parts.iter().enumerate() {
308            if part.is_empty() {
309                continue;
310            }
311            if let Some(found) = url[pos..].find(part) {
312                if i == 0 && found != 0 {
313                    return false;
314                }
315                pos += found + part.len();
316            } else {
317                return false;
318            }
319        }
320
321        // If pattern ends with *, any remaining URL is fine
322        // Otherwise, must have consumed all of URL
323        pattern.ends_with('*') || pos == url.len()
324    }
325}
326
327/// A captured network request
328#[derive(Debug, Clone, Serialize, Deserialize)]
329pub struct CapturedRequest {
330    /// Request URL
331    pub url: String,
332    /// HTTP method
333    pub method: HttpMethod,
334    /// Request headers
335    pub headers: HashMap<String, String>,
336    /// Request body
337    pub body: Option<Vec<u8>>,
338    /// Timestamp (milliseconds since interception start)
339    pub timestamp_ms: u64,
340}
341
342impl CapturedRequest {
343    /// Create a new captured request
344    #[must_use]
345    pub fn new(url: &str, method: HttpMethod, timestamp_ms: u64) -> Self {
346        Self {
347            url: url.to_string(),
348            method,
349            headers: HashMap::new(),
350            body: None,
351            timestamp_ms,
352        }
353    }
354
355    /// Get body as string
356    #[must_use]
357    pub fn body_string(&self) -> Option<String> {
358        self.body
359            .as_ref()
360            .map(|b| String::from_utf8_lossy(b).to_string())
361    }
362
363    /// Parse body as JSON
364    pub fn body_json<T: for<'de> Deserialize<'de>>(&self) -> ProbarResult<T> {
365        let body = self
366            .body
367            .as_ref()
368            .ok_or_else(|| ProbarError::AssertionFailed {
369                message: "No request body".to_string(),
370            })?;
371        let data = serde_json::from_slice(body)?;
372        Ok(data)
373    }
374}
375
376/// A route definition for interception
377#[derive(Debug, Clone)]
378pub struct Route {
379    /// URL pattern to match
380    pub pattern: UrlPattern,
381    /// HTTP method to match
382    pub method: HttpMethod,
383    /// Response to return
384    pub response: MockResponse,
385    /// Number of times this route should be used (None = unlimited)
386    pub times: Option<usize>,
387    /// Number of times this route has been matched
388    pub match_count: usize,
389}
390
391impl Route {
392    /// Create a new route
393    #[must_use]
394    pub fn new(pattern: UrlPattern, method: HttpMethod, response: MockResponse) -> Self {
395        Self {
396            pattern,
397            method,
398            response,
399            times: None,
400            match_count: 0,
401        }
402    }
403
404    /// Set how many times this route should match
405    #[must_use]
406    pub const fn times(mut self, n: usize) -> Self {
407        self.times = Some(n);
408        self
409    }
410
411    /// Check if this route matches a request
412    #[must_use]
413    pub fn matches(&self, url: &str, method: &HttpMethod) -> bool {
414        // Check if we've exceeded our match limit
415        if let Some(max) = self.times {
416            if self.match_count >= max {
417                return false;
418            }
419        }
420        self.pattern.matches(url) && self.method.matches(method)
421    }
422
423    /// Record a match
424    pub fn record_match(&mut self) {
425        self.match_count += 1;
426    }
427
428    /// Check if route is exhausted
429    #[must_use]
430    pub fn is_exhausted(&self) -> bool {
431        self.times.is_some_and(|max| self.match_count >= max)
432    }
433}
434
435/// Network interception handler
436#[derive(Debug)]
437pub struct NetworkInterception {
438    /// Registered routes
439    routes: Vec<Route>,
440    /// Captured requests
441    captured: Arc<Mutex<Vec<CapturedRequest>>>,
442    /// Whether to capture all requests (not just intercepted)
443    capture_all: bool,
444    /// Whether interception is active
445    active: bool,
446    /// Start timestamp
447    start_time: std::time::Instant,
448    /// Block unmatched requests
449    block_unmatched: bool,
450}
451
452impl Default for NetworkInterception {
453    fn default() -> Self {
454        Self::new()
455    }
456}
457
458impl NetworkInterception {
459    /// Create a new network interception handler
460    #[must_use]
461    pub fn new() -> Self {
462        Self {
463            routes: Vec::new(),
464            captured: Arc::new(Mutex::new(Vec::new())),
465            capture_all: false,
466            active: false,
467            start_time: std::time::Instant::now(),
468            block_unmatched: false,
469        }
470    }
471
472    /// Enable capturing all requests
473    #[must_use]
474    pub const fn capture_all(mut self) -> Self {
475        self.capture_all = true;
476        self
477    }
478
479    /// Block unmatched requests
480    #[must_use]
481    pub const fn block_unmatched(mut self) -> Self {
482        self.block_unmatched = true;
483        self
484    }
485
486    /// Start interception
487    pub fn start(&mut self) {
488        self.active = true;
489        self.start_time = std::time::Instant::now();
490    }
491
492    /// Stop interception
493    pub fn stop(&mut self) {
494        self.active = false;
495    }
496
497    /// Check if interception is active
498    #[must_use]
499    pub const fn is_active(&self) -> bool {
500        self.active
501    }
502
503    /// Add a route
504    pub fn route(&mut self, route: Route) {
505        self.routes.push(route);
506    }
507
508    /// Add a GET route
509    pub fn get(&mut self, pattern: &str, response: MockResponse) {
510        self.routes.push(Route::new(
511            UrlPattern::Contains(pattern.to_string()),
512            HttpMethod::Get,
513            response,
514        ));
515    }
516
517    /// Add a POST route
518    pub fn post(&mut self, pattern: &str, response: MockResponse) {
519        self.routes.push(Route::new(
520            UrlPattern::Contains(pattern.to_string()),
521            HttpMethod::Post,
522            response,
523        ));
524    }
525
526    /// Add a PUT route
527    pub fn put(&mut self, pattern: &str, response: MockResponse) {
528        self.routes.push(Route::new(
529            UrlPattern::Contains(pattern.to_string()),
530            HttpMethod::Put,
531            response,
532        ));
533    }
534
535    /// Add a DELETE route
536    pub fn delete(&mut self, pattern: &str, response: MockResponse) {
537        self.routes.push(Route::new(
538            UrlPattern::Contains(pattern.to_string()),
539            HttpMethod::Delete,
540            response,
541        ));
542    }
543
544    /// Handle an incoming request
545    pub fn handle_request(
546        &mut self,
547        url: &str,
548        method: HttpMethod,
549        headers: HashMap<String, String>,
550        body: Option<Vec<u8>>,
551    ) -> Option<MockResponse> {
552        if !self.active {
553            return None;
554        }
555
556        let timestamp_ms = self.start_time.elapsed().as_millis() as u64;
557
558        // Capture the request
559        if self.capture_all {
560            let mut request = CapturedRequest::new(url, method, timestamp_ms);
561            request.headers = headers.clone();
562            request.body = body.clone();
563            if let Ok(mut captured) = self.captured.lock() {
564                captured.push(request);
565            }
566        }
567
568        // Find matching route
569        for route in &mut self.routes {
570            if route.matches(url, &method) {
571                route.record_match();
572
573                // Capture matched request
574                if !self.capture_all {
575                    let mut request = CapturedRequest::new(url, method, timestamp_ms);
576                    request.headers = headers;
577                    request.body = body;
578                    if let Ok(mut captured) = self.captured.lock() {
579                        captured.push(request);
580                    }
581                }
582
583                return Some(route.response.clone());
584            }
585        }
586
587        // Return 404 if blocking unmatched requests
588        if self.block_unmatched {
589            Some(MockResponse::error(404, "No route matched"))
590        } else {
591            None
592        }
593    }
594
595    /// Get all captured requests
596    #[must_use]
597    pub fn captured_requests(&self) -> Vec<CapturedRequest> {
598        self.captured.lock().map(|c| c.clone()).unwrap_or_default()
599    }
600
601    /// Get captured requests matching a URL pattern
602    #[must_use]
603    pub fn requests_matching(&self, pattern: &UrlPattern) -> Vec<CapturedRequest> {
604        self.captured_requests()
605            .into_iter()
606            .filter(|r| pattern.matches(&r.url))
607            .collect()
608    }
609
610    /// Get captured requests by method
611    #[must_use]
612    pub fn requests_by_method(&self, method: HttpMethod) -> Vec<CapturedRequest> {
613        self.captured_requests()
614            .into_iter()
615            .filter(|r| r.method == method)
616            .collect()
617    }
618
619    /// Assert a request was made
620    pub fn assert_requested(&self, pattern: &UrlPattern) -> ProbarResult<()> {
621        let requests = self.requests_matching(pattern);
622        if requests.is_empty() {
623            return Err(ProbarError::AssertionFailed {
624                message: format!("Expected request matching {:?}, but none found", pattern),
625            });
626        }
627        Ok(())
628    }
629
630    /// Assert a request was made N times
631    pub fn assert_requested_times(&self, pattern: &UrlPattern, times: usize) -> ProbarResult<()> {
632        let requests = self.requests_matching(pattern);
633        if requests.len() != times {
634            return Err(ProbarError::AssertionFailed {
635                message: format!(
636                    "Expected {} requests matching {:?}, but found {}",
637                    times,
638                    pattern,
639                    requests.len()
640                ),
641            });
642        }
643        Ok(())
644    }
645
646    /// Assert no requests were made matching a pattern
647    pub fn assert_not_requested(&self, pattern: &UrlPattern) -> ProbarResult<()> {
648        let requests = self.requests_matching(pattern);
649        if !requests.is_empty() {
650            return Err(ProbarError::AssertionFailed {
651                message: format!(
652                    "Expected no requests matching {:?}, but found {}",
653                    pattern,
654                    requests.len()
655                ),
656            });
657        }
658        Ok(())
659    }
660
661    /// Clear captured requests
662    pub fn clear_captured(&self) {
663        if let Ok(mut captured) = self.captured.lock() {
664            captured.clear();
665        }
666    }
667
668    /// Get route count
669    #[must_use]
670    pub fn route_count(&self) -> usize {
671        self.routes.len()
672    }
673
674    /// Clear all routes
675    pub fn clear_routes(&mut self) {
676        self.routes.clear();
677    }
678
679    // =========================================================================
680    // PMAT-006: Network Abort & Wait Features (Playwright Parity)
681    // =========================================================================
682
683    /// Abort requests matching a pattern with a specific reason
684    ///
685    /// Per Playwright: `page.route('**/api/*', route => route.abort())`
686    pub fn abort(&mut self, pattern: &str, reason: AbortReason) {
687        let abort_response = MockResponse::new()
688            .with_status(0)
689            .with_body(reason.message().as_bytes().to_vec());
690        self.routes.push(Route::new(
691            UrlPattern::Contains(pattern.to_string()),
692            HttpMethod::Any,
693            abort_response,
694        ));
695    }
696
697    /// Abort requests matching a pattern (default reason: Aborted)
698    pub fn abort_pattern(&mut self, pattern: UrlPattern, reason: AbortReason) {
699        let abort_response = MockResponse::new()
700            .with_status(0)
701            .with_body(reason.message().as_bytes().to_vec());
702        self.routes
703            .push(Route::new(pattern, HttpMethod::Any, abort_response));
704    }
705
706    /// Wait for a request matching a pattern (synchronous check)
707    ///
708    /// Per Playwright: `page.waitForRequest(url)`
709    #[must_use]
710    pub fn find_request(&self, pattern: &UrlPattern) -> Option<CapturedRequest> {
711        self.requests_matching(pattern).into_iter().next()
712    }
713
714    /// Wait for a response matching a pattern (returns the response from route)
715    ///
716    /// Per Playwright: `page.waitForResponse(url)`
717    #[must_use]
718    pub fn find_response_for(&self, pattern: &UrlPattern) -> Option<MockResponse> {
719        for route in &self.routes {
720            if route.pattern.matches(&pattern.to_string()) || route.match_count > 0 {
721                return Some(route.response.clone());
722            }
723        }
724        None
725    }
726
727    /// Check if a request was aborted
728    #[must_use]
729    pub fn was_aborted(&self, pattern: &UrlPattern) -> bool {
730        for route in &self.routes {
731            if route.match_count > 0 && route.response.status == 0 {
732                if let UrlPattern::Contains(p) = &route.pattern {
733                    if pattern.matches(p) {
734                        return true;
735                    }
736                }
737            }
738        }
739        false
740    }
741
742    /// Get captured responses (mock responses that were returned)
743    #[must_use]
744    pub fn captured_responses(&self) -> Vec<MockResponse> {
745        self.routes
746            .iter()
747            .filter(|r| r.match_count > 0)
748            .map(|r| r.response.clone())
749            .collect()
750    }
751}
752
753impl std::fmt::Display for UrlPattern {
754    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
755        match self {
756            Self::Exact(s)
757            | Self::Prefix(s)
758            | Self::Contains(s)
759            | Self::Regex(s)
760            | Self::Glob(s) => write!(f, "{}", s),
761            Self::Any => write!(f, "*"),
762        }
763    }
764}
765
766/// Builder for creating network interception
767#[derive(Debug, Default)]
768pub struct NetworkInterceptionBuilder {
769    interception: NetworkInterception,
770}
771
772impl NetworkInterceptionBuilder {
773    /// Create a new builder
774    #[must_use]
775    pub fn new() -> Self {
776        Self::default()
777    }
778
779    /// Enable capturing all requests
780    #[must_use]
781    pub fn capture_all(mut self) -> Self {
782        self.interception.capture_all = true;
783        self
784    }
785
786    /// Block unmatched requests
787    #[must_use]
788    pub fn block_unmatched(mut self) -> Self {
789        self.interception.block_unmatched = true;
790        self
791    }
792
793    /// Add a GET route
794    #[must_use]
795    pub fn get(mut self, pattern: &str, response: MockResponse) -> Self {
796        self.interception.get(pattern, response);
797        self
798    }
799
800    /// Add a POST route
801    #[must_use]
802    pub fn post(mut self, pattern: &str, response: MockResponse) -> Self {
803        self.interception.post(pattern, response);
804        self
805    }
806
807    /// Add a custom route
808    #[must_use]
809    pub fn route(mut self, route: Route) -> Self {
810        self.interception.route(route);
811        self
812    }
813
814    /// Build the interception handler
815    #[must_use]
816    pub fn build(self) -> NetworkInterception {
817        self.interception
818    }
819}
820
821#[cfg(test)]
822#[allow(
823    clippy::unwrap_used,
824    clippy::expect_used,
825    clippy::panic,
826    clippy::default_trait_access
827)]
828mod tests {
829    use super::*;
830
831    mod http_method_tests {
832        use super::*;
833
834        #[test]
835        fn test_from_str() {
836            assert_eq!(HttpMethod::from_str("GET"), HttpMethod::Get);
837            assert_eq!(HttpMethod::from_str("post"), HttpMethod::Post);
838            assert_eq!(HttpMethod::from_str("PUT"), HttpMethod::Put);
839            assert_eq!(HttpMethod::from_str("DELETE"), HttpMethod::Delete);
840            assert_eq!(HttpMethod::from_str("unknown"), HttpMethod::Any);
841        }
842
843        #[test]
844        fn test_as_str() {
845            assert_eq!(HttpMethod::Get.as_str(), "GET");
846            assert_eq!(HttpMethod::Post.as_str(), "POST");
847            assert_eq!(HttpMethod::Any.as_str(), "*");
848        }
849
850        #[test]
851        fn test_matches() {
852            assert!(HttpMethod::Get.matches(&HttpMethod::Get));
853            assert!(HttpMethod::Any.matches(&HttpMethod::Get));
854            assert!(HttpMethod::Get.matches(&HttpMethod::Any));
855            assert!(!HttpMethod::Get.matches(&HttpMethod::Post));
856        }
857    }
858
859    mod mock_response_tests {
860        use super::*;
861
862        #[test]
863        fn test_default() {
864            let response = MockResponse::default();
865            assert_eq!(response.status, 200);
866            assert_eq!(response.content_type, "application/json");
867        }
868
869        #[test]
870        fn test_json() {
871            let data = serde_json::json!({"name": "test"});
872            let response = MockResponse::json(&data).unwrap();
873            assert_eq!(response.status, 200);
874            assert!(response.body_string().contains("test"));
875        }
876
877        #[test]
878        fn test_text() {
879            let response = MockResponse::text("Hello World");
880            assert_eq!(response.body_string(), "Hello World");
881            assert_eq!(response.content_type, "text/plain");
882        }
883
884        #[test]
885        fn test_error() {
886            let response = MockResponse::error(404, "Not Found");
887            assert_eq!(response.status, 404);
888            assert!(response.body_string().contains("Not Found"));
889        }
890
891        #[test]
892        fn test_with_status() {
893            let response = MockResponse::new().with_status(201);
894            assert_eq!(response.status, 201);
895        }
896
897        #[test]
898        fn test_with_header() {
899            let response = MockResponse::new().with_header("X-Custom", "value");
900            assert_eq!(response.headers.get("X-Custom"), Some(&"value".to_string()));
901        }
902
903        #[test]
904        fn test_with_delay() {
905            let response = MockResponse::new().with_delay(100);
906            assert_eq!(response.delay_ms, 100);
907        }
908    }
909
910    mod url_pattern_tests {
911        use super::*;
912
913        #[test]
914        fn test_exact() {
915            let pattern = UrlPattern::Exact("https://api.example.com/users".to_string());
916            assert!(pattern.matches("https://api.example.com/users"));
917            assert!(!pattern.matches("https://api.example.com/users/1"));
918        }
919
920        #[test]
921        fn test_prefix() {
922            let pattern = UrlPattern::Prefix("https://api.example.com".to_string());
923            assert!(pattern.matches("https://api.example.com/users"));
924            assert!(pattern.matches("https://api.example.com/posts"));
925            assert!(!pattern.matches("https://other.com"));
926        }
927
928        #[test]
929        fn test_contains() {
930            let pattern = UrlPattern::Contains("/api/".to_string());
931            assert!(pattern.matches("https://example.com/api/users"));
932            assert!(!pattern.matches("https://example.com/users"));
933        }
934
935        #[test]
936        fn test_regex() {
937            let pattern = UrlPattern::Regex(r"/users/\d+".to_string());
938            assert!(pattern.matches("https://api.example.com/users/123"));
939            assert!(!pattern.matches("https://api.example.com/users/abc"));
940        }
941
942        #[test]
943        fn test_glob() {
944            let pattern = UrlPattern::Glob("*/api/users/*".to_string());
945            assert!(pattern.matches("https://example.com/api/users/123"));
946            assert!(!pattern.matches("https://example.com/api/posts/123"));
947        }
948
949        #[test]
950        fn test_any() {
951            let pattern = UrlPattern::Any;
952            assert!(pattern.matches("anything"));
953            assert!(pattern.matches(""));
954        }
955    }
956
957    mod captured_request_tests {
958        use super::*;
959
960        #[test]
961        fn test_new() {
962            let request = CapturedRequest::new("https://api.example.com", HttpMethod::Get, 1000);
963            assert_eq!(request.url, "https://api.example.com");
964            assert_eq!(request.method, HttpMethod::Get);
965            assert_eq!(request.timestamp_ms, 1000);
966        }
967
968        #[test]
969        fn test_body_string() {
970            let mut request = CapturedRequest::new("url", HttpMethod::Post, 0);
971            request.body = Some(b"test body".to_vec());
972            assert_eq!(request.body_string(), Some("test body".to_string()));
973        }
974
975        #[test]
976        fn test_body_json() {
977            let mut request = CapturedRequest::new("url", HttpMethod::Post, 0);
978            request.body = Some(b"{\"name\":\"test\"}".to_vec());
979            let data: serde_json::Value = request.body_json().unwrap();
980            assert_eq!(data["name"], "test");
981        }
982    }
983
984    mod route_tests {
985        use super::*;
986
987        #[test]
988        fn test_new() {
989            let route = Route::new(
990                UrlPattern::Contains("/api".to_string()),
991                HttpMethod::Get,
992                MockResponse::new(),
993            );
994            assert_eq!(route.match_count, 0);
995            assert!(route.times.is_none());
996        }
997
998        #[test]
999        fn test_times() {
1000            let route = Route::new(UrlPattern::Any, HttpMethod::Get, MockResponse::new()).times(3);
1001            assert_eq!(route.times, Some(3));
1002        }
1003
1004        #[test]
1005        fn test_matches() {
1006            let route = Route::new(
1007                UrlPattern::Contains("/users".to_string()),
1008                HttpMethod::Get,
1009                MockResponse::new(),
1010            );
1011            assert!(route.matches("https://api.example.com/users", &HttpMethod::Get));
1012            assert!(!route.matches("https://api.example.com/users", &HttpMethod::Post));
1013            assert!(!route.matches("https://api.example.com/posts", &HttpMethod::Get));
1014        }
1015
1016        #[test]
1017        fn test_record_match() {
1018            let mut route = Route::new(UrlPattern::Any, HttpMethod::Any, MockResponse::new());
1019            route.record_match();
1020            assert_eq!(route.match_count, 1);
1021        }
1022
1023        #[test]
1024        fn test_is_exhausted() {
1025            let mut route =
1026                Route::new(UrlPattern::Any, HttpMethod::Any, MockResponse::new()).times(2);
1027
1028            assert!(!route.is_exhausted());
1029            route.record_match();
1030            assert!(!route.is_exhausted());
1031            route.record_match();
1032            assert!(route.is_exhausted());
1033        }
1034
1035        #[test]
1036        fn test_exhausted_route_no_longer_matches() {
1037            let mut route =
1038                Route::new(UrlPattern::Any, HttpMethod::Any, MockResponse::new()).times(1);
1039
1040            assert!(route.matches("url", &HttpMethod::Get));
1041            route.record_match();
1042            assert!(!route.matches("url", &HttpMethod::Get));
1043        }
1044    }
1045
1046    mod network_interception_tests {
1047        use super::*;
1048
1049        #[test]
1050        fn test_new() {
1051            let interception = NetworkInterception::new();
1052            assert!(!interception.is_active());
1053            assert_eq!(interception.route_count(), 0);
1054        }
1055
1056        #[test]
1057        fn test_start_stop() {
1058            let mut interception = NetworkInterception::new();
1059            interception.start();
1060            assert!(interception.is_active());
1061            interception.stop();
1062            assert!(!interception.is_active());
1063        }
1064
1065        #[test]
1066        fn test_add_routes() {
1067            let mut interception = NetworkInterception::new();
1068            interception.get("/api/users", MockResponse::text("users"));
1069            interception.post("/api/users", MockResponse::new().with_status(201));
1070            interception.put("/api/users/1", MockResponse::new());
1071            interception.delete("/api/users/1", MockResponse::new().with_status(204));
1072
1073            assert_eq!(interception.route_count(), 4);
1074        }
1075
1076        #[test]
1077        fn test_handle_request() {
1078            let mut interception = NetworkInterception::new();
1079            interception.get("/api/users", MockResponse::text("users list"));
1080            interception.start();
1081
1082            let response = interception.handle_request(
1083                "https://api.example.com/api/users",
1084                HttpMethod::Get,
1085                HashMap::new(),
1086                None,
1087            );
1088
1089            assert!(response.is_some());
1090            let response = response.unwrap();
1091            assert_eq!(response.body_string(), "users list");
1092        }
1093
1094        #[test]
1095        fn test_handle_request_no_match() {
1096            let mut interception = NetworkInterception::new();
1097            interception.get("/api/users", MockResponse::text("users"));
1098            interception.start();
1099
1100            let response = interception.handle_request(
1101                "https://api.example.com/api/posts",
1102                HttpMethod::Get,
1103                HashMap::new(),
1104                None,
1105            );
1106
1107            assert!(response.is_none());
1108        }
1109
1110        #[test]
1111        fn test_block_unmatched() {
1112            let mut interception = NetworkInterception::new().block_unmatched();
1113            interception.start();
1114
1115            let response = interception.handle_request(
1116                "https://api.example.com/unknown",
1117                HttpMethod::Get,
1118                HashMap::new(),
1119                None,
1120            );
1121
1122            assert!(response.is_some());
1123            assert_eq!(response.unwrap().status, 404);
1124        }
1125
1126        #[test]
1127        fn test_capture_requests() {
1128            let mut interception = NetworkInterception::new().capture_all();
1129            interception.get("/api/users", MockResponse::new());
1130            interception.start();
1131
1132            interception.handle_request(
1133                "https://api.example.com/api/users",
1134                HttpMethod::Get,
1135                HashMap::new(),
1136                None,
1137            );
1138
1139            let captured = interception.captured_requests();
1140            assert_eq!(captured.len(), 1);
1141            assert_eq!(captured[0].url, "https://api.example.com/api/users");
1142        }
1143
1144        #[test]
1145        fn test_requests_matching() {
1146            let mut interception = NetworkInterception::new().capture_all();
1147            interception.start();
1148
1149            interception.handle_request(
1150                "https://api.example.com/api/users",
1151                HttpMethod::Get,
1152                HashMap::new(),
1153                None,
1154            );
1155            interception.handle_request(
1156                "https://api.example.com/api/posts",
1157                HttpMethod::Get,
1158                HashMap::new(),
1159                None,
1160            );
1161
1162            let users = interception.requests_matching(&UrlPattern::Contains("/users".to_string()));
1163            assert_eq!(users.len(), 1);
1164        }
1165
1166        #[test]
1167        fn test_requests_by_method() {
1168            let mut interception = NetworkInterception::new().capture_all();
1169            interception.start();
1170
1171            interception.handle_request("url1", HttpMethod::Get, HashMap::new(), None);
1172            interception.handle_request("url2", HttpMethod::Post, HashMap::new(), None);
1173
1174            let gets = interception.requests_by_method(HttpMethod::Get);
1175            assert_eq!(gets.len(), 1);
1176        }
1177
1178        #[test]
1179        fn test_assert_requested() {
1180            let mut interception = NetworkInterception::new().capture_all();
1181            interception.start();
1182
1183            interception.handle_request(
1184                "https://api.example.com/api/users",
1185                HttpMethod::Get,
1186                HashMap::new(),
1187                None,
1188            );
1189
1190            assert!(interception
1191                .assert_requested(&UrlPattern::Contains("/users".to_string()))
1192                .is_ok());
1193
1194            assert!(interception
1195                .assert_requested(&UrlPattern::Contains("/posts".to_string()))
1196                .is_err());
1197        }
1198
1199        #[test]
1200        fn test_assert_requested_times() {
1201            let mut interception = NetworkInterception::new().capture_all();
1202            interception.start();
1203
1204            interception.handle_request("url", HttpMethod::Get, HashMap::new(), None);
1205            interception.handle_request("url", HttpMethod::Get, HashMap::new(), None);
1206
1207            assert!(interception
1208                .assert_requested_times(&UrlPattern::Any, 2)
1209                .is_ok());
1210
1211            assert!(interception
1212                .assert_requested_times(&UrlPattern::Any, 3)
1213                .is_err());
1214        }
1215
1216        #[test]
1217        fn test_assert_not_requested() {
1218            let interception = NetworkInterception::new().capture_all();
1219
1220            assert!(interception.assert_not_requested(&UrlPattern::Any).is_ok());
1221        }
1222
1223        #[test]
1224        fn test_clear_captured() {
1225            let mut interception = NetworkInterception::new().capture_all();
1226            interception.start();
1227
1228            interception.handle_request("url", HttpMethod::Get, HashMap::new(), None);
1229            assert_eq!(interception.captured_requests().len(), 1);
1230
1231            interception.clear_captured();
1232            assert_eq!(interception.captured_requests().len(), 0);
1233        }
1234
1235        #[test]
1236        fn test_clear_routes() {
1237            let mut interception = NetworkInterception::new();
1238            interception.get("/api", MockResponse::new());
1239            assert_eq!(interception.route_count(), 1);
1240
1241            interception.clear_routes();
1242            assert_eq!(interception.route_count(), 0);
1243        }
1244    }
1245
1246    mod network_interception_builder_tests {
1247        use super::*;
1248
1249        #[test]
1250        fn test_builder() {
1251            let interception = NetworkInterceptionBuilder::new()
1252                .capture_all()
1253                .block_unmatched()
1254                .get("/api/users", MockResponse::text("users"))
1255                .post("/api/users", MockResponse::new().with_status(201))
1256                .build();
1257
1258            assert!(interception.capture_all);
1259            assert!(interception.block_unmatched);
1260            assert_eq!(interception.route_count(), 2);
1261        }
1262
1263        #[test]
1264        fn test_builder_with_route() {
1265            let route = Route::new(
1266                UrlPattern::Regex(r"/users/\d+".to_string()),
1267                HttpMethod::Get,
1268                MockResponse::new(),
1269            );
1270
1271            let interception = NetworkInterceptionBuilder::new().route(route).build();
1272
1273            assert_eq!(interception.route_count(), 1);
1274        }
1275    }
1276
1277    // =========================================================================
1278    // PMAT-006: Network Abort & Wait Tests
1279    // =========================================================================
1280
1281    mod abort_tests {
1282        use super::*;
1283
1284        #[test]
1285        fn test_abort_reason_messages() {
1286            assert_eq!(AbortReason::Failed.message(), "net::ERR_FAILED");
1287            assert_eq!(AbortReason::Aborted.message(), "net::ERR_ABORTED");
1288            assert_eq!(AbortReason::TimedOut.message(), "net::ERR_TIMED_OUT");
1289            assert_eq!(
1290                AbortReason::AccessDenied.message(),
1291                "net::ERR_ACCESS_DENIED"
1292            );
1293            assert_eq!(
1294                AbortReason::ConnectionClosed.message(),
1295                "net::ERR_CONNECTION_CLOSED"
1296            );
1297            assert_eq!(
1298                AbortReason::ConnectionFailed.message(),
1299                "net::ERR_CONNECTION_FAILED"
1300            );
1301            assert_eq!(
1302                AbortReason::ConnectionRefused.message(),
1303                "net::ERR_CONNECTION_REFUSED"
1304            );
1305            assert_eq!(
1306                AbortReason::ConnectionReset.message(),
1307                "net::ERR_CONNECTION_RESET"
1308            );
1309            assert_eq!(
1310                AbortReason::InternetDisconnected.message(),
1311                "net::ERR_INTERNET_DISCONNECTED"
1312            );
1313            assert_eq!(
1314                AbortReason::NameNotResolved.message(),
1315                "net::ERR_NAME_NOT_RESOLVED"
1316            );
1317            assert_eq!(
1318                AbortReason::BlockedByClient.message(),
1319                "net::ERR_BLOCKED_BY_CLIENT"
1320            );
1321        }
1322
1323        #[test]
1324        fn test_abort_request() {
1325            let mut interception = NetworkInterception::new();
1326            interception.abort("/api/blocked", AbortReason::BlockedByClient);
1327            interception.start();
1328
1329            let response = interception.handle_request(
1330                "https://example.com/api/blocked/resource",
1331                HttpMethod::Get,
1332                HashMap::new(),
1333                None,
1334            );
1335
1336            assert!(response.is_some());
1337            let resp = response.unwrap();
1338            assert_eq!(resp.status, 0); // Aborted requests have status 0
1339            assert!(String::from_utf8_lossy(&resp.body).contains("ERR_BLOCKED_BY_CLIENT"));
1340        }
1341
1342        #[test]
1343        fn test_abort_pattern() {
1344            let mut interception = NetworkInterception::new();
1345            interception.abort_pattern(
1346                UrlPattern::Prefix("https://blocked.com".to_string()),
1347                AbortReason::AccessDenied,
1348            );
1349            interception.start();
1350
1351            let response = interception.handle_request(
1352                "https://blocked.com/any/path",
1353                HttpMethod::Get,
1354                HashMap::new(),
1355                None,
1356            );
1357
1358            assert!(response.is_some());
1359            assert_eq!(response.unwrap().status, 0);
1360        }
1361
1362        #[test]
1363        fn test_route_action_default() {
1364            let action: RouteAction = Default::default();
1365            assert!(matches!(action, RouteAction::Continue));
1366        }
1367
1368        #[test]
1369        fn test_route_action_respond() {
1370            let action = RouteAction::Respond(MockResponse::text("test"));
1371            if let RouteAction::Respond(resp) = action {
1372                assert_eq!(resp.body_string(), "test");
1373            } else {
1374                panic!("Expected Respond action");
1375            }
1376        }
1377
1378        #[test]
1379        fn test_route_action_abort() {
1380            let action = RouteAction::Abort(AbortReason::TimedOut);
1381            if let RouteAction::Abort(reason) = action {
1382                assert_eq!(reason, AbortReason::TimedOut);
1383            } else {
1384                panic!("Expected Abort action");
1385            }
1386        }
1387    }
1388
1389    mod wait_tests {
1390        use super::*;
1391
1392        #[test]
1393        fn test_find_request() {
1394            let mut interception = NetworkInterception::new().capture_all();
1395            interception.start();
1396
1397            interception.handle_request(
1398                "https://api.example.com/users/123",
1399                HttpMethod::Get,
1400                HashMap::new(),
1401                None,
1402            );
1403
1404            let request = interception.find_request(&UrlPattern::Contains("users".to_string()));
1405            assert!(request.is_some());
1406            assert!(request.unwrap().url.contains("users"));
1407
1408            let not_found = interception.find_request(&UrlPattern::Contains("posts".to_string()));
1409            assert!(not_found.is_none());
1410        }
1411
1412        #[test]
1413        fn test_find_response_for() {
1414            let mut interception = NetworkInterception::new();
1415            interception.get("/api/users", MockResponse::text("user data"));
1416            interception.start();
1417
1418            // Trigger the route
1419            interception.handle_request(
1420                "https://example.com/api/users",
1421                HttpMethod::Get,
1422                HashMap::new(),
1423                None,
1424            );
1425
1426            // Response should be found for matched routes
1427            let resp = interception.find_response_for(&UrlPattern::Contains("users".to_string()));
1428            assert!(resp.is_some());
1429        }
1430
1431        #[test]
1432        fn test_captured_responses() {
1433            let mut interception = NetworkInterception::new();
1434            interception.get("/api/users", MockResponse::text("users"));
1435            interception.post("/api/posts", MockResponse::text("posts"));
1436            interception.start();
1437
1438            // Only trigger one route
1439            interception.handle_request(
1440                "https://example.com/api/users",
1441                HttpMethod::Get,
1442                HashMap::new(),
1443                None,
1444            );
1445
1446            let responses = interception.captured_responses();
1447            assert_eq!(responses.len(), 1);
1448            assert_eq!(responses[0].body_string(), "users");
1449        }
1450
1451        #[test]
1452        fn test_url_pattern_to_string() {
1453            let exact = UrlPattern::Exact("https://example.com".to_string());
1454            let prefix = UrlPattern::Prefix("https://".to_string());
1455            let contains = UrlPattern::Contains("api".to_string());
1456            let regex = UrlPattern::Regex(r"\d+".to_string());
1457            let glob = UrlPattern::Glob("**/api/*".to_string());
1458            let any = UrlPattern::Any;
1459
1460            assert_eq!(exact.to_string(), "https://example.com");
1461            assert_eq!(prefix.to_string(), "https://");
1462            assert_eq!(contains.to_string(), "api");
1463            assert_eq!(regex.to_string(), r"\d+");
1464            assert_eq!(glob.to_string(), "**/api/*");
1465            assert_eq!(any.to_string(), "*");
1466        }
1467    }
1468
1469    // =========================================================================
1470    // Hâ‚€ EXTREME TDD: Network Interception Tests (Spec G.2 P0)
1471    // =========================================================================
1472
1473    mod h0_network_tests {
1474        use super::*;
1475
1476        #[test]
1477        fn h0_network_01_abort_reason_failed_message() {
1478            assert_eq!(AbortReason::Failed.message(), "net::ERR_FAILED");
1479        }
1480
1481        #[test]
1482        fn h0_network_02_abort_reason_timed_out() {
1483            assert_eq!(AbortReason::TimedOut.message(), "net::ERR_TIMED_OUT");
1484        }
1485
1486        #[test]
1487        fn h0_network_03_abort_reason_access_denied() {
1488            assert_eq!(
1489                AbortReason::AccessDenied.message(),
1490                "net::ERR_ACCESS_DENIED"
1491            );
1492        }
1493
1494        #[test]
1495        fn h0_network_04_abort_reason_connection_refused() {
1496            assert_eq!(
1497                AbortReason::ConnectionRefused.message(),
1498                "net::ERR_CONNECTION_REFUSED"
1499            );
1500        }
1501
1502        #[test]
1503        fn h0_network_05_abort_reason_internet_disconnected() {
1504            assert_eq!(
1505                AbortReason::InternetDisconnected.message(),
1506                "net::ERR_INTERNET_DISCONNECTED"
1507            );
1508        }
1509
1510        #[test]
1511        fn h0_network_06_route_action_default_continue() {
1512            let action: RouteAction = Default::default();
1513            assert!(matches!(action, RouteAction::Continue));
1514        }
1515
1516        #[test]
1517        fn h0_network_07_http_method_from_str_get() {
1518            let method = HttpMethod::from_str("GET");
1519            assert_eq!(method, HttpMethod::Get);
1520        }
1521
1522        #[test]
1523        fn h0_network_08_http_method_from_str_post() {
1524            let method = HttpMethod::from_str("POST");
1525            assert_eq!(method, HttpMethod::Post);
1526        }
1527
1528        #[test]
1529        fn h0_network_09_http_method_from_str_put() {
1530            let method = HttpMethod::from_str("PUT");
1531            assert_eq!(method, HttpMethod::Put);
1532        }
1533
1534        #[test]
1535        fn h0_network_10_http_method_from_str_delete() {
1536            let method = HttpMethod::from_str("DELETE");
1537            assert_eq!(method, HttpMethod::Delete);
1538        }
1539    }
1540
1541    mod h0_mock_response_tests {
1542        use super::*;
1543
1544        #[test]
1545        fn h0_network_11_mock_response_text() {
1546            let resp = MockResponse::text("hello");
1547            assert_eq!(resp.body_string(), "hello");
1548        }
1549
1550        #[test]
1551        fn h0_network_12_mock_response_json() {
1552            let resp = MockResponse::json(&serde_json::json!({"key": "value"})).unwrap();
1553            assert_eq!(resp.content_type, "application/json");
1554        }
1555
1556        #[test]
1557        fn h0_network_13_mock_response_error() {
1558            let resp = MockResponse::error(404, "Not Found");
1559            assert_eq!(resp.status, 404);
1560        }
1561
1562        #[test]
1563        fn h0_network_14_mock_response_status_200() {
1564            let resp = MockResponse::text("ok");
1565            assert_eq!(resp.status, 200);
1566        }
1567
1568        #[test]
1569        fn h0_network_15_mock_response_with_body() {
1570            let resp = MockResponse::new().with_body(vec![1, 2, 3]);
1571            assert_eq!(resp.body, vec![1, 2, 3]);
1572        }
1573
1574        #[test]
1575        fn h0_network_16_mock_response_with_header() {
1576            let resp = MockResponse::text("body").with_header("X-Custom", "value");
1577            assert_eq!(resp.headers.get("X-Custom"), Some(&"value".to_string()));
1578        }
1579
1580        #[test]
1581        fn h0_network_17_mock_response_with_content_type() {
1582            let resp = MockResponse::new().with_content_type("text/plain");
1583            assert_eq!(resp.content_type, "text/plain");
1584        }
1585
1586        #[test]
1587        fn h0_network_18_mock_response_default() {
1588            let resp = MockResponse::default();
1589            assert_eq!(resp.status, 200);
1590        }
1591
1592        #[test]
1593        fn h0_network_19_mock_response_clone() {
1594            let resp1 = MockResponse::text("cloned");
1595            let resp2 = resp1.clone();
1596            assert_eq!(resp1.body, resp2.body);
1597        }
1598
1599        #[test]
1600        fn h0_network_20_mock_response_debug() {
1601            let resp = MockResponse::text("test");
1602            let debug = format!("{:?}", resp);
1603            assert!(debug.contains("MockResponse"));
1604        }
1605    }
1606
1607    mod h0_url_pattern_tests {
1608        use super::*;
1609
1610        #[test]
1611        fn h0_network_21_url_pattern_exact_match() {
1612            let pattern = UrlPattern::Exact("https://example.com".to_string());
1613            assert!(pattern.matches("https://example.com"));
1614        }
1615
1616        #[test]
1617        fn h0_network_22_url_pattern_exact_no_match() {
1618            let pattern = UrlPattern::Exact("https://example.com".to_string());
1619            assert!(!pattern.matches("https://example.com/path"));
1620        }
1621
1622        #[test]
1623        fn h0_network_23_url_pattern_prefix_match() {
1624            let pattern = UrlPattern::Prefix("https://api.".to_string());
1625            assert!(pattern.matches("https://api.example.com/v1"));
1626        }
1627
1628        #[test]
1629        fn h0_network_24_url_pattern_contains_match() {
1630            let pattern = UrlPattern::Contains("/api/".to_string());
1631            assert!(pattern.matches("https://example.com/api/users"));
1632        }
1633
1634        #[test]
1635        fn h0_network_25_url_pattern_any_matches_all() {
1636            let pattern = UrlPattern::Any;
1637            assert!(pattern.matches("https://any-url.com/any/path"));
1638        }
1639
1640        #[test]
1641        fn h0_network_26_url_pattern_glob_single_star() {
1642            let pattern = UrlPattern::Glob("**/api/*".to_string());
1643            assert!(pattern.matches("https://example.com/api/users"));
1644        }
1645
1646        #[test]
1647        fn h0_network_27_url_pattern_regex() {
1648            let pattern = UrlPattern::Regex(r"/users/\d+".to_string());
1649            assert!(pattern.matches("https://api.com/users/123"));
1650        }
1651
1652        #[test]
1653        fn h0_network_28_url_pattern_to_string_exact() {
1654            let pattern = UrlPattern::Exact("test".to_string());
1655            assert_eq!(pattern.to_string(), "test");
1656        }
1657
1658        #[test]
1659        fn h0_network_29_url_pattern_to_string_any() {
1660            let pattern = UrlPattern::Any;
1661            assert_eq!(pattern.to_string(), "*");
1662        }
1663
1664        #[test]
1665        fn h0_network_30_url_pattern_clone() {
1666            let pattern1 = UrlPattern::Contains("api".to_string());
1667            let pattern2 = pattern1;
1668            assert!(pattern2.matches("https://api.com"));
1669        }
1670    }
1671
1672    mod h0_network_interception_tests {
1673        use super::*;
1674
1675        #[test]
1676        fn h0_network_31_interception_new() {
1677            let interception = NetworkInterception::new();
1678            assert!(!interception.is_active());
1679        }
1680
1681        #[test]
1682        fn h0_network_32_interception_start_stop() {
1683            let mut interception = NetworkInterception::new();
1684            interception.start();
1685            assert!(interception.is_active());
1686            interception.stop();
1687            assert!(!interception.is_active());
1688        }
1689
1690        #[test]
1691        fn h0_network_33_interception_get_route() {
1692            let mut interception = NetworkInterception::new();
1693            interception.get("/api/users", MockResponse::text("users"));
1694            assert_eq!(interception.route_count(), 1);
1695        }
1696
1697        #[test]
1698        fn h0_network_34_interception_post_route() {
1699            let mut interception = NetworkInterception::new();
1700            interception.post("/api/create", MockResponse::text("data"));
1701            assert_eq!(interception.route_count(), 1);
1702        }
1703
1704        #[test]
1705        fn h0_network_35_interception_put_route() {
1706            let mut interception = NetworkInterception::new();
1707            interception.put("/api/update", MockResponse::text("updated"));
1708            assert_eq!(interception.route_count(), 1);
1709        }
1710
1711        #[test]
1712        fn h0_network_36_interception_delete_route() {
1713            let mut interception = NetworkInterception::new();
1714            interception.delete("/api/remove", MockResponse::text("deleted"));
1715            assert_eq!(interception.route_count(), 1);
1716        }
1717
1718        #[test]
1719        fn h0_network_37_interception_abort() {
1720            let mut interception = NetworkInterception::new();
1721            interception.abort("/blocked", AbortReason::BlockedByClient);
1722            assert_eq!(interception.route_count(), 1);
1723        }
1724
1725        #[test]
1726        fn h0_network_38_interception_clear() {
1727            let mut interception = NetworkInterception::new();
1728            interception.get("/api", MockResponse::text("data"));
1729            interception.clear_routes();
1730            assert_eq!(interception.route_count(), 0);
1731        }
1732
1733        #[test]
1734        fn h0_network_39_interception_captured_requests() {
1735            let interception = NetworkInterception::new();
1736            assert!(interception.captured_requests().is_empty());
1737        }
1738
1739        #[test]
1740        fn h0_network_40_interception_captured_responses() {
1741            let interception = NetworkInterception::new();
1742            assert!(interception.captured_responses().is_empty());
1743        }
1744    }
1745
1746    mod h0_route_tests {
1747        use super::*;
1748
1749        #[test]
1750        fn h0_network_41_route_new() {
1751            let route = Route::new(
1752                UrlPattern::Contains("/api".to_string()),
1753                HttpMethod::Get,
1754                MockResponse::text("response"),
1755            );
1756            assert!(route.pattern.matches("https://example.com/api"));
1757        }
1758
1759        #[test]
1760        fn h0_network_42_route_matches_url_and_method() {
1761            let route = Route::new(
1762                UrlPattern::Contains("/api".to_string()),
1763                HttpMethod::Post,
1764                MockResponse::text("data"),
1765            );
1766            assert!(route.matches("https://example.com/api", &HttpMethod::Post));
1767        }
1768
1769        #[test]
1770        fn h0_network_43_route_no_match_wrong_method() {
1771            let route = Route::new(
1772                UrlPattern::Contains("/api".to_string()),
1773                HttpMethod::Get,
1774                MockResponse::text("data"),
1775            );
1776            assert!(!route.matches("https://example.com/api", &HttpMethod::Post));
1777        }
1778
1779        #[test]
1780        fn h0_network_44_route_record_match() {
1781            let mut route =
1782                Route::new(UrlPattern::Any, HttpMethod::Get, MockResponse::text("data"));
1783            route.record_match();
1784            assert_eq!(route.match_count, 1);
1785        }
1786
1787        #[test]
1788        fn h0_network_45_captured_request_new() {
1789            let request = CapturedRequest::new("https://api.com/users", HttpMethod::Get, 1000);
1790            assert_eq!(request.url, "https://api.com/users");
1791            assert_eq!(request.method, HttpMethod::Get);
1792        }
1793
1794        #[test]
1795        fn h0_network_46_captured_request_body_string() {
1796            let mut request = CapturedRequest::new("https://api.com", HttpMethod::Post, 0);
1797            request.body = Some(b"hello".to_vec());
1798            assert_eq!(request.body_string(), Some("hello".to_string()));
1799        }
1800
1801        #[test]
1802        fn h0_network_47_http_method_matches_same() {
1803            assert!(HttpMethod::Get.matches(&HttpMethod::Get));
1804        }
1805
1806        #[test]
1807        fn h0_network_48_http_method_matches_any() {
1808            assert!(HttpMethod::Any.matches(&HttpMethod::Get));
1809            assert!(HttpMethod::Any.matches(&HttpMethod::Post));
1810        }
1811
1812        #[test]
1813        fn h0_network_49_http_method_no_match_different() {
1814            assert!(!HttpMethod::Get.matches(&HttpMethod::Post));
1815        }
1816
1817        #[test]
1818        fn h0_network_50_abort_reason_all_variants() {
1819            let reasons = vec![
1820                AbortReason::Failed,
1821                AbortReason::Aborted,
1822                AbortReason::TimedOut,
1823                AbortReason::AccessDenied,
1824                AbortReason::ConnectionClosed,
1825                AbortReason::ConnectionFailed,
1826                AbortReason::ConnectionRefused,
1827                AbortReason::ConnectionReset,
1828                AbortReason::InternetDisconnected,
1829                AbortReason::NameNotResolved,
1830                AbortReason::BlockedByClient,
1831            ];
1832            for reason in reasons {
1833                assert!(!reason.message().is_empty());
1834            }
1835        }
1836    }
1837}