Skip to main content

fastapi_core/
testing.rs

1//! Test utilities for fastapi applications.
2//!
3//! This module provides a [`TestClient`] for in-process testing of handlers
4//! without network overhead. It integrates with asupersync's capability model
5//! and supports deterministic testing via the Lab runtime.
6//!
7//! # Features
8//!
9//! - **In-process testing**: No network I/O, fast execution
10//! - **HTTP-like API**: Familiar `client.get("/path")` interface
11//! - **Request builder**: Fluent API for headers, body, cookies
12//! - **Response assertions**: Convenient assertion helpers
13//! - **Cookie jar**: Automatic session management across requests
14//! - **Lab integration**: Deterministic testing with asupersync
15//!
16//! # Example
17//!
18//! ```ignore
19//! use fastapi_core::testing::TestClient;
20//! use fastapi_core::middleware::Handler;
21//!
22//! async fn hello_handler(ctx: &RequestContext, req: &mut Request) -> Response {
23//!     Response::ok().body(ResponseBody::Bytes(b"Hello, World!".to_vec()))
24//! }
25//!
26//! #[test]
27//! fn test_hello() {
28//!     let client = TestClient::new(hello_handler);
29//!     let response = client.get("/hello").send();
30//!
31//!     assert_eq!(response.status().as_u16(), 200);
32//!     assert_eq!(response.text(), "Hello, World!");
33//! }
34//! ```
35//!
36//! # Deterministic Testing
37//!
38//! For reproducible tests involving concurrency, use [`TestClient::with_seed`]:
39//!
40//! ```ignore
41//! let client = TestClient::with_seed(handler, 42);
42//! // Same seed = same execution order for concurrent operations
43//! ```
44
45use parking_lot::Mutex;
46use std::collections::HashMap;
47use std::future::Future;
48use std::sync::Arc;
49
50use asupersync::Cx;
51
52use crate::context::RequestContext;
53use crate::dependency::{DependencyOverrides, FromDependency};
54use crate::middleware::Handler;
55use crate::request::{Body, Method, Request};
56use crate::response::{Response, ResponseBody, StatusCode};
57
58/// A simple cookie jar for maintaining cookies across requests.
59///
60/// Cookies are stored as name-value pairs and automatically
61/// added to subsequent requests.
62#[derive(Debug, Clone, Default)]
63pub struct CookieJar {
64    cookies: Vec<StoredCookie>,
65    next_id: u64,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69enum CookieSameSite {
70    Lax,
71    Strict,
72    None,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76enum CookieDomain {
77    /// Unscoped cookies are always sent (used by manual `CookieJar::set()`).
78    Any,
79    /// Host-only cookie (Domain attribute not present).
80    HostOnly(String),
81    /// Domain cookie (Domain attribute present).
82    Domain(String),
83}
84
85#[derive(Debug, Clone)]
86struct StoredCookie {
87    id: u64,
88    name: String,
89    value: String,
90    domain: CookieDomain,
91    path: String,
92    secure: bool,
93    #[allow(dead_code)]
94    http_only: bool,
95    #[allow(dead_code)]
96    same_site: Option<CookieSameSite>,
97    expires_at: Option<std::time::SystemTime>,
98}
99
100impl CookieJar {
101    /// Creates an empty cookie jar.
102    #[must_use]
103    pub fn new() -> Self {
104        Self::default()
105    }
106
107    /// Sets a cookie in the jar.
108    pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
109        let name = name.into();
110        let value = value.into();
111
112        // Preserve previous semantics: manual `set()` behaves like a single cookie per name.
113        self.cookies
114            .retain(|c| !(c.name == name && c.domain == CookieDomain::Any));
115
116        let id = self.next_id;
117        self.next_id = self.next_id.wrapping_add(1);
118        self.cookies.push(StoredCookie {
119            id,
120            name,
121            value,
122            domain: CookieDomain::Any,
123            path: "/".to_string(),
124            secure: false,
125            http_only: false,
126            same_site: None,
127            expires_at: None,
128        });
129    }
130
131    /// Gets a cookie value by name.
132    #[must_use]
133    pub fn get(&self, name: &str) -> Option<&str> {
134        self.cookies
135            .iter()
136            .filter(|c| c.name == name)
137            .max_by_key(|c| c.id)
138            .map(|c| c.value.as_str())
139    }
140
141    /// Removes a cookie from the jar.
142    pub fn remove(&mut self, name: &str) -> Option<String> {
143        let mut removed: Option<String> = None;
144        self.cookies.retain(|c| {
145            if c.name == name {
146                removed = Some(c.value.clone());
147                false
148            } else {
149                true
150            }
151        });
152        removed
153    }
154
155    /// Clears all cookies from the jar.
156    pub fn clear(&mut self) {
157        self.cookies.clear();
158        self.next_id = 0;
159    }
160
161    /// Returns the number of cookies in the jar.
162    #[must_use]
163    pub fn len(&self) -> usize {
164        self.cookies.len()
165    }
166
167    /// Returns `true` if the jar is empty.
168    #[must_use]
169    pub fn is_empty(&self) -> bool {
170        self.cookies.is_empty()
171    }
172
173    /// Formats cookies for the Cookie header.
174    #[must_use]
175    pub fn to_cookie_header(&self) -> Option<String> {
176        let mut by_name: HashMap<&str, &StoredCookie> = HashMap::new();
177        for c in &self.cookies {
178            match by_name.get(c.name.as_str()) {
179                Some(existing) if existing.id >= c.id => {}
180                _ => {
181                    by_name.insert(c.name.as_str(), c);
182                }
183            }
184        }
185
186        if by_name.is_empty() {
187            return None;
188        }
189
190        Some(
191            by_name
192                .into_values()
193                .map(|c| format!("{}={}", c.name, c.value))
194                .collect::<Vec<_>>()
195                .join("; "),
196        )
197    }
198
199    /// Formats cookies for the Cookie header for a specific request.
200    ///
201    /// This applies the cookie matching rules needed for session persistence:
202    /// domain, path, secure, and expiration.
203    #[must_use]
204    pub fn cookie_header_for_request(&self, request: &Request) -> Option<String> {
205        let host = request_host(request);
206        let path = request.path();
207        let is_secure = request_is_secure(request);
208        let now = std::time::SystemTime::now();
209
210        // Select one cookie per name using path length (more specific wins), then newest.
211        let mut selected: HashMap<&str, &StoredCookie> = HashMap::new();
212        for c in &self.cookies {
213            if c.secure && !is_secure {
214                continue;
215            }
216            if let Some(exp) = c.expires_at {
217                if exp <= now {
218                    continue;
219                }
220            }
221            if !domain_matches(&c.domain, host.as_deref()) {
222                continue;
223            }
224            if !path_matches(&c.path, path) {
225                continue;
226            }
227
228            match selected.get(c.name.as_str()) {
229                None => {
230                    selected.insert(c.name.as_str(), c);
231                }
232                Some(existing) => {
233                    let a = (c.path.len(), c.id);
234                    let b = (existing.path.len(), existing.id);
235                    if a > b {
236                        selected.insert(c.name.as_str(), c);
237                    }
238                }
239            }
240        }
241
242        if selected.is_empty() {
243            return None;
244        }
245
246        Some(
247            selected
248                .into_values()
249                .map(|c| format!("{}={}", c.name, c.value))
250                .collect::<Vec<_>>()
251                .join("; "),
252        )
253    }
254
255    /// Parses a Set-Cookie header and updates the jar.
256    pub fn parse_set_cookie(&mut self, request: &Request, header_value: &[u8]) {
257        let Ok(value) = std::str::from_utf8(header_value) else {
258            return;
259        };
260        self.parse_set_cookie_str(request, value);
261    }
262
263    fn parse_set_cookie_str(&mut self, request: &Request, value: &str) {
264        let mut parts = value.split(';');
265        let Some((name, val)) = parse_cookie_name_value(parts.next()) else {
266            return;
267        };
268
269        let host = request_host(request);
270        let attrs = parse_set_cookie_attrs(parts);
271
272        let Some(domain) = cookie_domain_for_set_cookie(host.as_deref(), attrs.domain) else {
273            return;
274        };
275
276        let path = attrs
277            .path
278            .unwrap_or_else(|| default_cookie_path(request.path()));
279
280        let expires_at = match compute_cookie_expiration(attrs.max_age, attrs.expires_at) {
281            CookieExpiration::Delete => {
282                self.remove_by_key(name, &domain, &path);
283                return;
284            }
285            CookieExpiration::Keep(expires_at) => expires_at,
286        };
287
288        let id = self.next_id;
289        self.next_id = self.next_id.wrapping_add(1);
290        self.upsert(StoredCookie {
291            id,
292            name: name.to_string(),
293            value: val.to_string(),
294            domain,
295            path,
296            secure: attrs.secure,
297            http_only: attrs.http_only,
298            same_site: attrs.same_site,
299            expires_at,
300        });
301    }
302
303    fn remove_by_key(&mut self, name: &str, domain: &CookieDomain, path: &str) {
304        self.cookies
305            .retain(|c| !(c.name == name && &c.domain == domain && c.path == path));
306    }
307
308    fn upsert(&mut self, cookie: StoredCookie) {
309        // Replace existing cookie with same (name, domain, path), otherwise insert.
310        for existing in &mut self.cookies {
311            if existing.name == cookie.name
312                && existing.domain == cookie.domain
313                && existing.path == cookie.path
314            {
315                *existing = cookie;
316                return;
317            }
318        }
319        self.cookies.push(cookie);
320    }
321}
322
323#[derive(Debug, Default)]
324struct SetCookieAttrs {
325    domain: Option<String>,
326    path: Option<String>,
327    max_age: Option<i64>,
328    secure: bool,
329    http_only: bool,
330    same_site: Option<CookieSameSite>,
331    expires_at: Option<std::time::SystemTime>,
332}
333
334#[derive(Debug, Clone, Copy)]
335enum CookieExpiration {
336    Delete,
337    Keep(Option<std::time::SystemTime>),
338}
339
340fn parse_cookie_name_value(first: Option<&str>) -> Option<(&str, &str)> {
341    let first = first?;
342    let (name, val) = first.split_once('=')?;
343    let name = name.trim();
344    if name.is_empty() {
345        return None;
346    }
347    Some((name, val.trim()))
348}
349
350fn parse_set_cookie_attrs<'a>(parts: impl Iterator<Item = &'a str>) -> SetCookieAttrs {
351    let mut attrs = SetCookieAttrs::default();
352    for raw in parts {
353        let raw = raw.trim();
354        if raw.is_empty() {
355            continue;
356        }
357        if let Some((k, v)) = raw.split_once('=') {
358            let k = k.trim().to_ascii_lowercase();
359            let v = v.trim();
360            match k.as_str() {
361                "domain" => {
362                    let mut d = v.trim_matches('"').trim().to_ascii_lowercase();
363                    if let Some(stripped) = d.strip_prefix('.') {
364                        d = stripped.to_string();
365                    }
366                    if !d.is_empty() {
367                        attrs.domain = Some(d);
368                    }
369                }
370                "path" => {
371                    let p = v.trim_matches('"').trim();
372                    if p.starts_with('/') {
373                        attrs.path = Some(p.to_string());
374                    }
375                }
376                "max-age" => {
377                    if let Ok(n) = v.parse::<i64>() {
378                        attrs.max_age = Some(n);
379                    }
380                }
381                "samesite" => {
382                    let ss = v.trim_matches('"').trim();
383                    attrs.same_site = match ss.to_ascii_lowercase().as_str() {
384                        "lax" => Some(CookieSameSite::Lax),
385                        "strict" => Some(CookieSameSite::Strict),
386                        "none" => Some(CookieSameSite::None),
387                        _ => None,
388                    };
389                }
390                "expires" => {
391                    if let Some(t) = parse_http_date(v) {
392                        attrs.expires_at = Some(t);
393                    }
394                }
395                _ => {}
396            }
397        } else {
398            match raw.to_ascii_lowercase().as_str() {
399                "secure" => attrs.secure = true,
400                "httponly" => attrs.http_only = true,
401                _ => {}
402            }
403        }
404    }
405    attrs
406}
407
408fn cookie_domain_for_set_cookie(
409    host: Option<&str>,
410    domain_attr: Option<String>,
411) -> Option<CookieDomain> {
412    match domain_attr {
413        Some(d) => {
414            let h = host?;
415            let domain = CookieDomain::Domain(d);
416            if domain_matches(&domain, Some(h)) {
417                Some(domain)
418            } else {
419                None
420            }
421        }
422        None => {
423            let h = host?;
424            Some(CookieDomain::HostOnly(h.to_string()))
425        }
426    }
427}
428
429fn compute_cookie_expiration(
430    max_age: Option<i64>,
431    mut expires_at: Option<std::time::SystemTime>,
432) -> CookieExpiration {
433    let now = std::time::SystemTime::now();
434    if let Some(n) = max_age {
435        if n <= 0 {
436            return CookieExpiration::Delete;
437        }
438        let Ok(secs) = u64::try_from(n) else {
439            return CookieExpiration::Delete;
440        };
441        expires_at = now.checked_add(std::time::Duration::from_secs(secs));
442    }
443    if let Some(exp) = expires_at {
444        if exp <= now {
445            return CookieExpiration::Delete;
446        }
447    }
448    CookieExpiration::Keep(expires_at)
449}
450
451fn request_host(request: &Request) -> Option<String> {
452    let host = request.headers().get("host")?;
453    let s = std::str::from_utf8(host).ok()?;
454    let host = s.trim();
455    if host.is_empty() {
456        return None;
457    }
458    // Strip port if present.
459    Some(host.split(':').next().unwrap_or(host).to_ascii_lowercase())
460}
461
462fn request_is_secure(request: &Request) -> bool {
463    if let Some(info) = request.get_extension::<crate::request::ConnectionInfo>() {
464        if info.is_tls {
465            return true;
466        }
467    }
468
469    if let Some(forwarded) = request.headers().get("forwarded") {
470        if let Ok(s) = std::str::from_utf8(forwarded) {
471            for entry in s.split(',') {
472                for param in entry.split(';') {
473                    let param = param.trim();
474                    if let Some((k, v)) = param.split_once('=') {
475                        if k.trim().eq_ignore_ascii_case("proto") {
476                            let proto = v.trim().trim_matches('"');
477                            if proto.eq_ignore_ascii_case("https") {
478                                return true;
479                            }
480                        }
481                    }
482                }
483            }
484        }
485    }
486
487    if let Some(proto) = request.headers().get("x-forwarded-proto") {
488        let first = proto.split(|&b| b == b',').next().unwrap_or(proto);
489        let first = trim_ascii_bytes(first);
490        return first.eq_ignore_ascii_case(b"https");
491    }
492    if let Some(ssl) = request.headers().get("x-forwarded-ssl") {
493        return ssl.eq_ignore_ascii_case(b"on");
494    }
495    if let Some(https) = request.headers().get("front-end-https") {
496        return https.eq_ignore_ascii_case(b"on");
497    }
498
499    false
500}
501
502fn trim_ascii_bytes(mut bytes: &[u8]) -> &[u8] {
503    while matches!(bytes.first(), Some(b' ' | b'\t')) {
504        bytes = &bytes[1..];
505    }
506    while matches!(bytes.last(), Some(b' ' | b'\t')) {
507        bytes = &bytes[..bytes.len() - 1];
508    }
509    bytes
510}
511
512fn default_cookie_path(request_path: &str) -> String {
513    // RFC 6265 default-path algorithm (5.1.4).
514    if !request_path.starts_with('/') {
515        return "/".to_string();
516    }
517    if request_path == "/" {
518        return "/".to_string();
519    }
520    match request_path.rfind('/') {
521        Some(0) | None => "/".to_string(),
522        Some(idx) => request_path[..idx].to_string(),
523    }
524}
525
526fn domain_matches(domain: &CookieDomain, host: Option<&str>) -> bool {
527    match domain {
528        CookieDomain::Any => true,
529        CookieDomain::HostOnly(d) => host.is_some_and(|h| h.eq_ignore_ascii_case(d)),
530        CookieDomain::Domain(d) => {
531            let Some(h) = host else { return false };
532            if h.eq_ignore_ascii_case(d) {
533                return true;
534            }
535            // Suffix match with dot boundary.
536            h.len() > d.len() && h.ends_with(d) && h.as_bytes()[h.len() - d.len() - 1] == b'.'
537        }
538    }
539}
540
541fn path_matches(cookie_path: &str, request_path: &str) -> bool {
542    if cookie_path == "/" {
543        return request_path.starts_with('/');
544    }
545    if !request_path.starts_with(cookie_path) {
546        return false;
547    }
548    if cookie_path.ends_with('/') {
549        return true;
550    }
551    request_path
552        .as_bytes()
553        .get(cookie_path.len())
554        .is_none_or(|&b| b == b'/')
555}
556
557fn parse_http_date(input: &str) -> Option<std::time::SystemTime> {
558    // Parse IMF-fixdate: "Wed, 21 Oct 2015 07:28:00 GMT"
559    // We intentionally keep this minimal; invalid dates are ignored per RFC6265.
560    let s = input.trim().trim_matches('"').trim();
561    let (_dow, rest) = s.split_once(',')?;
562    let rest = rest.trim();
563    let mut it = rest.split_whitespace();
564    let day = it.next()?.parse::<u32>().ok()?;
565    let month = match it.next()? {
566        "Jan" => 1,
567        "Feb" => 2,
568        "Mar" => 3,
569        "Apr" => 4,
570        "May" => 5,
571        "Jun" => 6,
572        "Jul" => 7,
573        "Aug" => 8,
574        "Sep" => 9,
575        "Oct" => 10,
576        "Nov" => 11,
577        "Dec" => 12,
578        _ => return None,
579    };
580    let year = it.next()?.parse::<i32>().ok()?;
581    let time = it.next()?;
582    let tz = it.next()?;
583    if tz != "GMT" {
584        return None;
585    }
586    let (hh, mm, ss) = {
587        let mut t = time.split(':');
588        let hh = t.next()?.parse::<u32>().ok()?;
589        let mm = t.next()?.parse::<u32>().ok()?;
590        let ss = t.next()?.parse::<u32>().ok()?;
591        (hh, mm, ss)
592    };
593
594    // Convert to unix timestamp using a small civil->days function.
595    fn days_from_civil(y: i32, m: u32, d: u32) -> i64 {
596        // Howard Hinnant's algorithm.
597        let y = i64::from(y) - i64::from(m <= 2);
598        let era = (if y >= 0 { y } else { y - 399 }) / 400;
599        let yoe = y - era * 400;
600        let m = i64::from(m);
601        let doy = (153 * (m + if m > 2 { -3 } else { 9 }) + 2) / 5 + i64::from(d) - 1;
602        let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
603        era * 146097 + doe - 719468
604    }
605
606    let days = days_from_civil(year, month, day);
607    let secs = days
608        .checked_mul(86_400)?
609        .checked_add(i64::from(hh) * 3600 + i64::from(mm) * 60 + i64::from(ss))?;
610    if secs < 0 {
611        return None;
612    }
613    let secs_u64 = u64::try_from(secs).ok()?;
614    Some(std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs_u64))
615}
616
617/// Test client for in-process HTTP testing.
618///
619/// `TestClient` wraps a handler and provides an HTTP-like interface
620/// for testing without network overhead. It maintains a cookie jar
621/// for session persistence across requests.
622///
623/// # Thread Safety
624///
625/// `TestClient` is thread-safe and can be shared across test threads.
626/// The internal cookie jar is protected by a mutex.
627///
628/// # Example
629///
630/// ```ignore
631/// let client = TestClient::new(my_handler);
632///
633/// // Simple GET request
634/// let response = client.get("/users").send();
635/// assert_eq!(response.status(), StatusCode::OK);
636///
637/// // POST with JSON body
638/// let response = client
639///     .post("/users")
640///     .json(&CreateUser { name: "Alice" })
641///     .send();
642/// assert_eq!(response.status(), StatusCode::CREATED);
643///
644/// // Request with headers
645/// let response = client
646///     .get("/protected")
647///     .header("Authorization", "Bearer token123")
648///     .send();
649/// ```
650pub struct TestClient<H> {
651    handler: Arc<H>,
652    cookies: Arc<Mutex<CookieJar>>,
653    dependency_overrides: Arc<DependencyOverrides>,
654    seed: Option<u64>,
655    request_id_counter: Arc<std::sync::atomic::AtomicU64>,
656}
657
658impl<H: Handler + 'static> TestClient<H> {
659    /// Creates a new test client wrapping the given handler.
660    ///
661    /// # Example
662    ///
663    /// ```ignore
664    /// let client = TestClient::new(my_handler);
665    /// ```
666    pub fn new(handler: H) -> Self {
667        let dependency_overrides = handler
668            .dependency_overrides()
669            .unwrap_or_else(|| Arc::new(DependencyOverrides::new()));
670        Self {
671            handler: Arc::new(handler),
672            cookies: Arc::new(Mutex::new(CookieJar::new())),
673            dependency_overrides,
674            seed: None,
675            request_id_counter: Arc::new(std::sync::atomic::AtomicU64::new(1)),
676        }
677    }
678
679    /// Creates a test client with a deterministic seed for the Lab runtime.
680    ///
681    /// Using the same seed produces identical execution order for
682    /// concurrent operations, enabling reproducible test failures.
683    ///
684    /// # Example
685    ///
686    /// ```ignore
687    /// let client = TestClient::with_seed(my_handler, 42);
688    /// ```
689    pub fn with_seed(handler: H, seed: u64) -> Self {
690        let dependency_overrides = handler
691            .dependency_overrides()
692            .unwrap_or_else(|| Arc::new(DependencyOverrides::new()));
693        Self {
694            handler: Arc::new(handler),
695            cookies: Arc::new(Mutex::new(CookieJar::new())),
696            dependency_overrides,
697            seed: Some(seed),
698            request_id_counter: Arc::new(std::sync::atomic::AtomicU64::new(1)),
699        }
700    }
701
702    /// Returns the seed used for deterministic testing, if set.
703    #[must_use]
704    pub fn seed(&self) -> Option<u64> {
705        self.seed
706    }
707
708    /// Returns a reference to the cookie jar.
709    ///
710    /// Note: The jar is protected by a mutex, so concurrent access
711    /// is safe but may block.
712    pub fn cookies(&self) -> parking_lot::MutexGuard<'_, CookieJar> {
713        self.cookies.lock()
714    }
715
716    /// Clears all cookies from the jar.
717    pub fn clear_cookies(&self) {
718        self.cookies().clear();
719    }
720
721    /// Creates a GET request builder.
722    #[must_use]
723    pub fn get(&self, path: &str) -> RequestBuilder<'_, H> {
724        RequestBuilder::new(self, Method::Get, path)
725    }
726
727    /// Creates a POST request builder.
728    #[must_use]
729    pub fn post(&self, path: &str) -> RequestBuilder<'_, H> {
730        RequestBuilder::new(self, Method::Post, path)
731    }
732
733    /// Creates a PUT request builder.
734    #[must_use]
735    pub fn put(&self, path: &str) -> RequestBuilder<'_, H> {
736        RequestBuilder::new(self, Method::Put, path)
737    }
738
739    /// Creates a DELETE request builder.
740    #[must_use]
741    pub fn delete(&self, path: &str) -> RequestBuilder<'_, H> {
742        RequestBuilder::new(self, Method::Delete, path)
743    }
744
745    /// Creates a PATCH request builder.
746    #[must_use]
747    pub fn patch(&self, path: &str) -> RequestBuilder<'_, H> {
748        RequestBuilder::new(self, Method::Patch, path)
749    }
750
751    /// Creates an OPTIONS request builder.
752    #[must_use]
753    pub fn options(&self, path: &str) -> RequestBuilder<'_, H> {
754        RequestBuilder::new(self, Method::Options, path)
755    }
756
757    /// Creates a HEAD request builder.
758    #[must_use]
759    pub fn head(&self, path: &str) -> RequestBuilder<'_, H> {
760        RequestBuilder::new(self, Method::Head, path)
761    }
762
763    /// Creates a request builder with a custom method.
764    #[must_use]
765    pub fn request(&self, method: Method, path: &str) -> RequestBuilder<'_, H> {
766        RequestBuilder::new(self, method, path)
767    }
768
769    /// Register a dependency override for this test client.
770    pub fn override_dependency<T, F, Fut>(&self, f: F)
771    where
772        T: FromDependency,
773        F: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
774        Fut: Future<Output = Result<T, T::Error>> + Send + 'static,
775    {
776        self.dependency_overrides.insert::<T, F, Fut>(f);
777    }
778
779    /// Register a fixed dependency override value.
780    pub fn override_dependency_value<T>(&self, value: T)
781    where
782        T: FromDependency,
783    {
784        self.dependency_overrides.insert_value(value);
785    }
786
787    /// Clear all registered dependency overrides.
788    pub fn clear_dependency_overrides(&self) {
789        self.dependency_overrides.clear();
790    }
791
792    /// Generates a unique request ID for tracing.
793    fn next_request_id(&self) -> u64 {
794        self.request_id_counter
795            .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
796    }
797
798    /// Executes a request and returns the response.
799    ///
800    /// This is called internally by `RequestBuilder::send()`.
801    fn execute(&self, mut request: Request) -> TestResponse {
802        // Many features (cookies, redirects, absolute URL building) require a host.
803        // In tests, default to a stable host if one wasn't provided.
804        if !request.headers().contains("host") {
805            request.headers_mut().insert("host", b"testserver".to_vec());
806        }
807
808        // Add cookies from jar to request
809        {
810            let jar = self.cookies();
811            if let Some(cookie_header) = jar.cookie_header_for_request(&request) {
812                request
813                    .headers_mut()
814                    .insert("cookie", cookie_header.into_bytes());
815            }
816        }
817
818        // Create test context with Cx::for_testing()
819        let cx = Cx::for_testing();
820        let request_id = self.next_request_id();
821        let ctx =
822            RequestContext::with_overrides(cx, request_id, Arc::clone(&self.dependency_overrides));
823
824        // The TestClient API is synchronous; run the async handler to completion.
825        let response = futures_executor::block_on(self.handler.call(&ctx, &mut request));
826
827        // Extract cookies from response
828        {
829            let mut jar = self.cookies();
830            for (name, value) in response.headers() {
831                if name.eq_ignore_ascii_case("set-cookie") {
832                    jar.parse_set_cookie(&request, value);
833                }
834            }
835        }
836
837        TestResponse::new(response, request_id)
838    }
839}
840
841impl<H> Clone for TestClient<H> {
842    fn clone(&self) -> Self {
843        Self {
844            handler: Arc::clone(&self.handler),
845            cookies: Arc::clone(&self.cookies),
846            dependency_overrides: Arc::clone(&self.dependency_overrides),
847            seed: self.seed,
848            request_id_counter: Arc::clone(&self.request_id_counter),
849        }
850    }
851}
852
853/// Builder for constructing test requests with a fluent API.
854///
855/// Use the methods on [`TestClient`] to create a request builder,
856/// then chain configuration methods and call [`send`](Self::send) to execute.
857///
858/// # Example
859///
860/// ```ignore
861/// let response = client
862///     .post("/api/items")
863///     .header("Content-Type", "application/json")
864///     .body(r#"{"name": "Widget"}"#)
865///     .send();
866/// ```
867pub struct RequestBuilder<'a, H> {
868    client: &'a TestClient<H>,
869    method: Method,
870    path: String,
871    query: Option<String>,
872    headers: Vec<(String, Vec<u8>)>,
873    body: Body,
874}
875
876impl<'a, H: Handler + 'static> RequestBuilder<'a, H> {
877    /// Creates a new request builder.
878    fn new(client: &'a TestClient<H>, method: Method, path: &str) -> Self {
879        // Split path and query string
880        let (path, query) = if let Some(idx) = path.find('?') {
881            (path[..idx].to_string(), Some(path[idx + 1..].to_string()))
882        } else {
883            (path.to_string(), None)
884        };
885
886        Self {
887            client,
888            method,
889            path,
890            query,
891            headers: Vec::new(),
892            body: Body::Empty,
893        }
894    }
895
896    /// Sets a query string parameter.
897    ///
898    /// Multiple calls append parameters.
899    ///
900    /// # Example
901    ///
902    /// ```ignore
903    /// client.get("/search").query("q", "rust").query("limit", "10").send()
904    /// ```
905    #[must_use]
906    pub fn query(mut self, key: &str, value: &str) -> Self {
907        let param = format!("{key}={value}");
908        self.query = Some(match self.query {
909            Some(q) => format!("{q}&{param}"),
910            None => param,
911        });
912        self
913    }
914
915    /// Sets a request header.
916    ///
917    /// # Example
918    ///
919    /// ```ignore
920    /// client.get("/api").header("Authorization", "Bearer token").send()
921    /// ```
922    #[must_use]
923    pub fn header(mut self, name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
924        self.headers.push((name.into(), value.into()));
925        self
926    }
927
928    /// Sets a request header with a string value.
929    #[must_use]
930    pub fn header_str(self, name: impl Into<String>, value: &str) -> Self {
931        self.header(name, value.as_bytes().to_vec())
932    }
933
934    /// Sets the request body as raw bytes.
935    ///
936    /// # Example
937    ///
938    /// ```ignore
939    /// client.post("/upload").body(b"binary data".to_vec()).send()
940    /// ```
941    #[must_use]
942    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
943        self.body = Body::Bytes(body.into());
944        self
945    }
946
947    /// Sets the request body as a string.
948    #[must_use]
949    pub fn body_str(self, body: &str) -> Self {
950        self.body(body.as_bytes().to_vec())
951    }
952
953    /// Sets the request body as JSON.
954    ///
955    /// Automatically sets the Content-Type header to `application/json`.
956    ///
957    /// # Example
958    ///
959    /// ```ignore
960    /// #[derive(Serialize)]
961    /// struct CreateUser { name: String }
962    ///
963    /// client.post("/users").json(&CreateUser { name: "Alice".into() }).send()
964    /// ```
965    #[must_use]
966    pub fn json<T: serde::Serialize>(mut self, value: &T) -> Self {
967        let bytes = serde_json::to_vec(value).expect("JSON serialization failed");
968        self.body = Body::Bytes(bytes);
969        self.headers
970            .push(("content-type".to_string(), b"application/json".to_vec()));
971        self
972    }
973
974    /// Sets a cookie for this request only.
975    ///
976    /// This does not affect the client's cookie jar.
977    #[must_use]
978    pub fn cookie(self, name: &str, value: &str) -> Self {
979        let cookie = format!("{name}={value}");
980        self.header("cookie", cookie.into_bytes())
981    }
982
983    /// Sends the request and returns the response.
984    ///
985    /// # Example
986    ///
987    /// ```ignore
988    /// let response = client.get("/").send();
989    /// ```
990    #[must_use]
991    pub fn send(self) -> TestResponse {
992        let mut request = Request::new(self.method, self.path);
993        request.set_query(self.query);
994        request.set_body(self.body);
995
996        for (name, value) in self.headers {
997            request.headers_mut().insert(name, value);
998        }
999
1000        self.client.execute(request)
1001    }
1002}
1003
1004/// Response from a test request with assertion helpers.
1005///
1006/// `TestResponse` wraps a [`Response`] and provides convenient methods
1007/// for accessing response data and making assertions in tests.
1008///
1009/// # Example
1010///
1011/// ```ignore
1012/// let response = client.get("/api/user").send();
1013///
1014/// assert_eq!(response.status(), StatusCode::OK);
1015/// assert!(response.header("content-type").contains("application/json"));
1016///
1017/// let user: User = response.json().unwrap();
1018/// assert_eq!(user.name, "Alice");
1019/// ```
1020#[derive(Debug)]
1021pub struct TestResponse {
1022    inner: Response,
1023    request_id: u64,
1024}
1025
1026impl TestResponse {
1027    /// Creates a new test response.
1028    fn new(response: Response, request_id: u64) -> Self {
1029        Self {
1030            inner: response,
1031            request_id,
1032        }
1033    }
1034
1035    /// Returns the request ID for tracing.
1036    #[must_use]
1037    pub fn request_id(&self) -> u64 {
1038        self.request_id
1039    }
1040
1041    /// Returns the HTTP status code.
1042    #[must_use]
1043    pub fn status(&self) -> StatusCode {
1044        self.inner.status()
1045    }
1046
1047    /// Returns the status code as a u16.
1048    #[must_use]
1049    pub fn status_code(&self) -> u16 {
1050        self.inner.status().as_u16()
1051    }
1052
1053    /// Checks if the status is successful (2xx).
1054    #[must_use]
1055    pub fn is_success(&self) -> bool {
1056        let code = self.status_code();
1057        (200..300).contains(&code)
1058    }
1059
1060    /// Checks if the status is a redirect (3xx).
1061    #[must_use]
1062    pub fn is_redirect(&self) -> bool {
1063        let code = self.status_code();
1064        (300..400).contains(&code)
1065    }
1066
1067    /// Checks if the status is a client error (4xx).
1068    #[must_use]
1069    pub fn is_client_error(&self) -> bool {
1070        let code = self.status_code();
1071        (400..500).contains(&code)
1072    }
1073
1074    /// Checks if the status is a server error (5xx).
1075    #[must_use]
1076    pub fn is_server_error(&self) -> bool {
1077        let code = self.status_code();
1078        (500..600).contains(&code)
1079    }
1080
1081    /// Returns all headers.
1082    #[must_use]
1083    pub fn headers(&self) -> &[(String, Vec<u8>)] {
1084        self.inner.headers()
1085    }
1086
1087    /// Returns a header value by name (case-insensitive).
1088    #[must_use]
1089    pub fn header(&self, name: &str) -> Option<&[u8]> {
1090        let name_lower = name.to_ascii_lowercase();
1091        self.inner
1092            .headers()
1093            .iter()
1094            .find(|(n, _)| n.to_ascii_lowercase() == name_lower)
1095            .map(|(_, v)| v.as_slice())
1096    }
1097
1098    /// Returns a header value as a string (case-insensitive).
1099    #[must_use]
1100    pub fn header_str(&self, name: &str) -> Option<&str> {
1101        self.header(name).and_then(|v| std::str::from_utf8(v).ok())
1102    }
1103
1104    /// Returns the Content-Type header value.
1105    #[must_use]
1106    pub fn content_type(&self) -> Option<&str> {
1107        self.header_str("content-type")
1108    }
1109
1110    /// Returns the body as raw bytes.
1111    #[must_use]
1112    pub fn bytes(&self) -> &[u8] {
1113        match self.inner.body_ref() {
1114            ResponseBody::Empty => &[],
1115            ResponseBody::Bytes(b) => b,
1116            ResponseBody::Stream(_) => {
1117                panic!("streaming response body not supported in TestResponse")
1118            }
1119        }
1120    }
1121
1122    /// Returns the body as a UTF-8 string.
1123    ///
1124    /// # Panics
1125    ///
1126    /// Panics if the body is not valid UTF-8.
1127    #[must_use]
1128    pub fn text(&self) -> &str {
1129        std::str::from_utf8(self.bytes()).expect("response body is not valid UTF-8")
1130    }
1131
1132    /// Tries to return the body as a UTF-8 string.
1133    #[must_use]
1134    pub fn text_opt(&self) -> Option<&str> {
1135        std::str::from_utf8(self.bytes()).ok()
1136    }
1137
1138    /// Parses the body as JSON.
1139    ///
1140    /// # Errors
1141    ///
1142    /// Returns an error if the body cannot be parsed as the target type.
1143    ///
1144    /// # Example
1145    ///
1146    /// ```ignore
1147    /// #[derive(Deserialize)]
1148    /// struct User { name: String }
1149    ///
1150    /// let user: User = response.json().unwrap();
1151    /// ```
1152    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
1153        serde_json::from_slice(self.bytes())
1154    }
1155
1156    /// Returns the body length.
1157    #[must_use]
1158    pub fn content_length(&self) -> usize {
1159        self.bytes().len()
1160    }
1161
1162    /// Returns the underlying response.
1163    #[must_use]
1164    pub fn into_inner(self) -> Response {
1165        self.inner
1166    }
1167
1168    // =========================================================================
1169    // Assertion Helpers
1170    // =========================================================================
1171
1172    /// Asserts that the status code equals the expected value.
1173    ///
1174    /// # Panics
1175    ///
1176    /// Panics with a descriptive message if the assertion fails.
1177    #[must_use]
1178    pub fn assert_status(&self, expected: StatusCode) -> &Self {
1179        assert_eq!(
1180            self.status(),
1181            expected,
1182            "Expected status {}, got {} for request {}",
1183            expected.as_u16(),
1184            self.status_code(),
1185            self.request_id
1186        );
1187        self
1188    }
1189
1190    /// Asserts that the status code equals the expected u16 value.
1191    ///
1192    /// # Panics
1193    ///
1194    /// Panics with a descriptive message if the assertion fails.
1195    #[must_use]
1196    pub fn assert_status_code(&self, expected: u16) -> &Self {
1197        assert_eq!(
1198            self.status_code(),
1199            expected,
1200            "Expected status {expected}, got {} for request {}",
1201            self.status_code(),
1202            self.request_id
1203        );
1204        self
1205    }
1206
1207    /// Asserts that the response is successful (2xx).
1208    ///
1209    /// # Panics
1210    ///
1211    /// Panics if the status is not in the 2xx range.
1212    #[must_use]
1213    pub fn assert_success(&self) -> &Self {
1214        assert!(
1215            self.is_success(),
1216            "Expected success status, got {} for request {}",
1217            self.status_code(),
1218            self.request_id
1219        );
1220        self
1221    }
1222
1223    /// Asserts that a header exists with the given value.
1224    ///
1225    /// # Panics
1226    ///
1227    /// Panics if the header doesn't exist or doesn't match.
1228    #[must_use]
1229    pub fn assert_header(&self, name: &str, expected: &str) -> &Self {
1230        let actual = self.header_str(name);
1231        assert_eq!(
1232            actual,
1233            Some(expected),
1234            "Expected header '{name}' to be '{expected}', got {:?} for request {}",
1235            actual,
1236            self.request_id
1237        );
1238        self
1239    }
1240
1241    /// Asserts that the body equals the expected string.
1242    ///
1243    /// # Panics
1244    ///
1245    /// Panics if the body doesn't match.
1246    #[must_use]
1247    pub fn assert_text(&self, expected: &str) -> &Self {
1248        assert_eq!(
1249            self.text(),
1250            expected,
1251            "Body mismatch for request {}",
1252            self.request_id
1253        );
1254        self
1255    }
1256
1257    /// Asserts that the body contains the expected substring.
1258    ///
1259    /// # Panics
1260    ///
1261    /// Panics if the body doesn't contain the substring.
1262    #[must_use]
1263    pub fn assert_text_contains(&self, expected: &str) -> &Self {
1264        assert!(
1265            self.text().contains(expected),
1266            "Expected body to contain '{}', got '{}' for request {}",
1267            expected,
1268            self.text(),
1269            self.request_id
1270        );
1271        self
1272    }
1273
1274    /// Asserts that the JSON body equals the expected value.
1275    ///
1276    /// # Panics
1277    ///
1278    /// Panics if parsing fails or the value doesn't match.
1279    #[must_use]
1280    pub fn assert_json<T>(&self, expected: &T) -> &Self
1281    where
1282        T: serde::de::DeserializeOwned + serde::Serialize + PartialEq + std::fmt::Debug,
1283    {
1284        let actual: T = self.json().expect("Failed to parse response as JSON");
1285        assert_eq!(
1286            actual, *expected,
1287            "JSON body mismatch for request {}",
1288            self.request_id
1289        );
1290        self
1291    }
1292
1293    /// Asserts that the JSON body contains all fields from the expected value.
1294    ///
1295    /// This performs partial matching: the actual response may contain additional
1296    /// fields not present in `expected`, but all fields in `expected` must be
1297    /// present in the actual response with matching values.
1298    ///
1299    /// # Panics
1300    ///
1301    /// Panics if parsing fails or partial matching fails.
1302    ///
1303    /// # Example
1304    ///
1305    /// ```ignore
1306    /// // Response body: {"id": 1, "name": "Alice", "email": "alice@example.com"}
1307    /// // This passes because all expected fields match:
1308    /// response.assert_json_contains(&json!({"name": "Alice"}));
1309    /// ```
1310    #[must_use]
1311    pub fn assert_json_contains(&self, expected: &serde_json::Value) -> &Self {
1312        let actual: serde_json::Value = self.json().expect("Failed to parse response as JSON");
1313        if let Err(path) = json_contains(&actual, expected) {
1314            panic!(
1315                "JSON partial match failed at path '{}' for request {}\n\
1316                 Expected (partial):\n{}\n\
1317                 Actual:\n{}",
1318                path,
1319                self.request_id,
1320                serde_json::to_string_pretty(expected).unwrap_or_else(|_| format!("{expected:?}")),
1321                serde_json::to_string_pretty(&actual).unwrap_or_else(|_| format!("{actual:?}")),
1322            );
1323        }
1324        self
1325    }
1326
1327    /// Asserts that the body matches the given regex pattern.
1328    ///
1329    /// # Panics
1330    ///
1331    /// Panics if the pattern doesn't match or is invalid.
1332    ///
1333    /// # Example
1334    ///
1335    /// ```ignore
1336    /// response.assert_body_matches(r"user_\d+");
1337    /// ```
1338    #[cfg(feature = "regex")]
1339    #[must_use]
1340    pub fn assert_body_matches(&self, pattern: &str) -> &Self {
1341        let re = regex::Regex::new(pattern)
1342            .unwrap_or_else(|e| panic!("Invalid regex pattern '{pattern}': {e}"));
1343        let body = self.text();
1344        assert!(
1345            re.is_match(body),
1346            "Expected body to match pattern '{}', got '{}' for request {}",
1347            pattern,
1348            body,
1349            self.request_id
1350        );
1351        self
1352    }
1353
1354    /// Asserts that a header exists and matches the given regex pattern.
1355    ///
1356    /// # Panics
1357    ///
1358    /// Panics if the header doesn't exist, can't be read as UTF-8,
1359    /// or doesn't match the pattern.
1360    #[cfg(feature = "regex")]
1361    #[must_use]
1362    pub fn assert_header_matches(&self, name: &str, pattern: &str) -> &Self {
1363        let re = regex::Regex::new(pattern)
1364            .unwrap_or_else(|e| panic!("Invalid regex pattern '{pattern}': {e}"));
1365        let value = self
1366            .header_str(name)
1367            .unwrap_or_else(|| panic!("Header '{name}' not found for request {}", self.request_id));
1368        assert!(
1369            re.is_match(value),
1370            "Expected header '{}' to match pattern '{}', got '{}' for request {}",
1371            name,
1372            pattern,
1373            value,
1374            self.request_id
1375        );
1376        self
1377    }
1378
1379    /// Asserts that a header exists (regardless of value).
1380    ///
1381    /// # Panics
1382    ///
1383    /// Panics if the header doesn't exist.
1384    #[must_use]
1385    pub fn assert_header_exists(&self, name: &str) -> &Self {
1386        assert!(
1387            self.header(name).is_some(),
1388            "Expected header '{}' to exist for request {}",
1389            name,
1390            self.request_id
1391        );
1392        self
1393    }
1394
1395    /// Asserts that a header does not exist.
1396    ///
1397    /// # Panics
1398    ///
1399    /// Panics if the header exists.
1400    #[must_use]
1401    pub fn assert_header_missing(&self, name: &str) -> &Self {
1402        assert!(
1403            self.header(name).is_none(),
1404            "Expected header '{}' to not exist for request {}, but found {:?}",
1405            name,
1406            self.request_id,
1407            self.header_str(name)
1408        );
1409        self
1410    }
1411
1412    /// Asserts that the Content-Type header contains the expected value.
1413    ///
1414    /// This is a convenience method that checks if the Content-Type header
1415    /// contains the given string (useful for checking media types ignoring charset).
1416    ///
1417    /// # Panics
1418    ///
1419    /// Panics if the Content-Type header doesn't exist or doesn't contain the expected value.
1420    #[must_use]
1421    pub fn assert_content_type_contains(&self, expected: &str) -> &Self {
1422        let ct = self.content_type().unwrap_or_else(|| {
1423            panic!(
1424                "Content-Type header not found for request {}",
1425                self.request_id
1426            )
1427        });
1428        assert!(
1429            ct.contains(expected),
1430            "Expected Content-Type to contain '{}', got '{}' for request {}",
1431            expected,
1432            ct,
1433            self.request_id
1434        );
1435        self
1436    }
1437}
1438
1439// =============================================================================
1440// Partial JSON Matching
1441// =============================================================================
1442
1443/// Checks if `actual` contains all fields from `expected`.
1444///
1445/// Returns `Ok(())` if matching succeeds, or `Err(path)` where `path` is
1446/// the JSON path to the first mismatch.
1447///
1448/// # Matching Rules
1449///
1450/// - **Objects**: All keys in `expected` must exist in `actual` with matching values.
1451///   Extra keys in `actual` are ignored.
1452/// - **Arrays**: Must match exactly (same length, same elements in order).
1453/// - **Primitives**: Must be equal.
1454///
1455/// # Example
1456///
1457/// ```
1458/// use fastapi_core::testing::json_contains;
1459/// use serde_json::json;
1460///
1461/// // Partial match succeeds - actual has extra "email" field
1462/// let actual = json!({"id": 1, "name": "Alice", "email": "alice@example.com"});
1463/// let expected = json!({"name": "Alice"});
1464/// assert!(json_contains(&actual, &expected).is_ok());
1465///
1466/// // Mismatch fails
1467/// let expected = json!({"name": "Bob"});
1468/// assert!(json_contains(&actual, &expected).is_err());
1469/// ```
1470pub fn json_contains(
1471    actual: &serde_json::Value,
1472    expected: &serde_json::Value,
1473) -> Result<(), String> {
1474    json_contains_at_path(actual, expected, "$")
1475}
1476
1477fn json_contains_at_path(
1478    actual: &serde_json::Value,
1479    expected: &serde_json::Value,
1480    path: &str,
1481) -> Result<(), String> {
1482    use serde_json::Value;
1483
1484    match (actual, expected) {
1485        // For objects, check that all expected keys exist with matching values
1486        (Value::Object(actual_obj), Value::Object(expected_obj)) => {
1487            for (key, expected_val) in expected_obj {
1488                let child_path = format!("{path}.{key}");
1489                match actual_obj.get(key) {
1490                    Some(actual_val) => {
1491                        json_contains_at_path(actual_val, expected_val, &child_path)?;
1492                    }
1493                    None => {
1494                        return Err(child_path);
1495                    }
1496                }
1497            }
1498            Ok(())
1499        }
1500        // For arrays, require exact match (partial array matching is ambiguous)
1501        (Value::Array(actual_arr), Value::Array(expected_arr)) => {
1502            if actual_arr.len() != expected_arr.len() {
1503                return Err(format!("{path}[length]"));
1504            }
1505            for (i, (actual_elem, expected_elem)) in
1506                actual_arr.iter().zip(expected_arr.iter()).enumerate()
1507            {
1508                let child_path = format!("{path}[{i}]");
1509                json_contains_at_path(actual_elem, expected_elem, &child_path)?;
1510            }
1511            Ok(())
1512        }
1513        // For primitives, require exact match
1514        _ => {
1515            if actual == expected {
1516                Ok(())
1517            } else {
1518                Err(path.to_string())
1519            }
1520        }
1521    }
1522}
1523
1524// =============================================================================
1525// Helper Traits for Assertion Macros
1526// =============================================================================
1527
1528/// Helper trait to convert various types to u16 for status code comparison.
1529///
1530/// This enables the `assert_status!` macro to accept both `u16` literals
1531/// and `StatusCode` values.
1532pub trait IntoStatusU16 {
1533    fn into_status_u16(self) -> u16;
1534}
1535
1536impl IntoStatusU16 for u16 {
1537    fn into_status_u16(self) -> u16 {
1538        self
1539    }
1540}
1541
1542impl IntoStatusU16 for StatusCode {
1543    fn into_status_u16(self) -> u16 {
1544        self.as_u16()
1545    }
1546}
1547
1548// Also implement for i32 since integer literals without suffix default to i32
1549impl IntoStatusU16 for i32 {
1550    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1551    fn into_status_u16(self) -> u16 {
1552        // This is intentional - HTTP status codes are always 3-digit positive numbers
1553        self as u16
1554    }
1555}
1556
1557// =============================================================================
1558// Assertion Macros
1559// =============================================================================
1560
1561/// Asserts that a test response has the expected HTTP status code.
1562///
1563/// Accepts either a `u16` literal or a `StatusCode` value.
1564///
1565/// # Examples
1566///
1567/// ```ignore
1568/// use fastapi_core::assert_status;
1569///
1570/// let response = client.get("/users").send();
1571/// assert_status!(response, 200);
1572/// assert_status!(response, StatusCode::OK);
1573/// ```
1574///
1575/// With custom message:
1576/// ```ignore
1577/// assert_status!(response, 404, "User should not be found");
1578/// ```
1579#[macro_export]
1580macro_rules! assert_status {
1581    ($response:expr, $expected:expr) => {{
1582        let response = &$response;
1583        let actual = response.status_code();
1584        // Use a trait to handle both u16 and StatusCode
1585        let expected_code: u16 = $crate::testing::IntoStatusU16::into_status_u16($expected);
1586        if actual != expected_code {
1587            panic!(
1588                "assertion failed: `(response.status() == {})`\n\
1589                 expected status: {}\n\
1590                 actual status: {}\n\
1591                 request id: {}\n\
1592                 response body: {}",
1593                expected_code,
1594                expected_code,
1595                actual,
1596                response.request_id(),
1597                response.text_opt().unwrap_or("<non-UTF8 body>")
1598            );
1599        }
1600    }};
1601    ($response:expr, $expected:expr, $($msg:tt)+) => {{
1602        let response = &$response;
1603        let actual = response.status_code();
1604        let expected_code: u16 = $crate::testing::IntoStatusU16::into_status_u16($expected);
1605        if actual != expected_code {
1606            panic!(
1607                "{}\n\
1608                 assertion failed: `(response.status() == {})`\n\
1609                 expected status: {}\n\
1610                 actual status: {}\n\
1611                 request id: {}\n\
1612                 response body: {}",
1613                format_args!($($msg)+),
1614                expected_code,
1615                expected_code,
1616                actual,
1617                response.request_id(),
1618                response.text_opt().unwrap_or("<non-UTF8 body>")
1619            );
1620        }
1621    }};
1622}
1623
1624/// Asserts that a test response has a header with the expected value.
1625///
1626/// # Examples
1627///
1628/// ```ignore
1629/// use fastapi_core::assert_header;
1630///
1631/// let response = client.get("/api").send();
1632/// assert_header!(response, "Content-Type", "application/json");
1633/// ```
1634///
1635/// With custom message:
1636/// ```ignore
1637/// assert_header!(response, "X-Custom", "value", "Custom header should be set");
1638/// ```
1639#[macro_export]
1640macro_rules! assert_header {
1641    ($response:expr, $name:expr, $expected:expr) => {{
1642        let response = &$response;
1643        let name = $name;
1644        let expected = $expected;
1645        let actual = response.header_str(name);
1646        if actual != Some(expected) {
1647            panic!(
1648                "assertion failed: `(response.header(\"{}\") == \"{}\")`\n\
1649                 expected header '{}': \"{}\"\n\
1650                 actual header '{}': {:?}\n\
1651                 request id: {}",
1652                name,
1653                expected,
1654                name,
1655                expected,
1656                name,
1657                actual,
1658                response.request_id()
1659            );
1660        }
1661    }};
1662    ($response:expr, $name:expr, $expected:expr, $($msg:tt)+) => {{
1663        let response = &$response;
1664        let name = $name;
1665        let expected = $expected;
1666        let actual = response.header_str(name);
1667        if actual != Some(expected) {
1668            panic!(
1669                "{}\n\
1670                 assertion failed: `(response.header(\"{}\") == \"{}\")`\n\
1671                 expected header '{}': \"{}\"\n\
1672                 actual header '{}': {:?}\n\
1673                 request id: {}",
1674                format_args!($($msg)+),
1675                name,
1676                expected,
1677                name,
1678                expected,
1679                name,
1680                actual,
1681                response.request_id()
1682            );
1683        }
1684    }};
1685}
1686
1687/// Asserts that a test response body contains the expected substring.
1688///
1689/// # Examples
1690///
1691/// ```ignore
1692/// use fastapi_core::assert_body_contains;
1693///
1694/// let response = client.get("/hello").send();
1695/// assert_body_contains!(response, "Hello");
1696/// ```
1697///
1698/// With custom message:
1699/// ```ignore
1700/// assert_body_contains!(response, "error", "Response should contain error message");
1701/// ```
1702#[macro_export]
1703macro_rules! assert_body_contains {
1704    ($response:expr, $expected:expr) => {{
1705        let response = &$response;
1706        let expected = $expected;
1707        let body = response.text();
1708        if !body.contains(expected) {
1709            panic!(
1710                "assertion failed: response body does not contain \"{}\"\n\
1711                 expected substring: \"{}\"\n\
1712                 actual body: \"{}\"\n\
1713                 request id: {}",
1714                expected, expected, body, response.request_id()
1715            );
1716        }
1717    }};
1718    ($response:expr, $expected:expr, $($msg:tt)+) => {{
1719        let response = &$response;
1720        let expected = $expected;
1721        let body = response.text();
1722        if !body.contains(expected) {
1723            panic!(
1724                "{}\n\
1725                 assertion failed: response body does not contain \"{}\"\n\
1726                 expected substring: \"{}\"\n\
1727                 actual body: \"{}\"\n\
1728                 request id: {}",
1729                format_args!($($msg)+),
1730                expected,
1731                expected,
1732                body,
1733                response.request_id()
1734            );
1735        }
1736    }};
1737}
1738
1739/// Asserts that a test response body matches the expected JSON value (partial match).
1740///
1741/// This macro performs partial JSON matching: the actual response may contain
1742/// additional fields not present in `expected`, but all fields in `expected`
1743/// must be present with matching values.
1744///
1745/// # Examples
1746///
1747/// ```ignore
1748/// use fastapi_core::assert_json;
1749/// use serde_json::json;
1750///
1751/// let response = client.get("/user/1").send();
1752/// // Response: {"id": 1, "name": "Alice", "email": "alice@example.com"}
1753///
1754/// // Exact match
1755/// assert_json!(response, {"id": 1, "name": "Alice", "email": "alice@example.com"});
1756///
1757/// // Partial match (ignores email field)
1758/// assert_json!(response, {"name": "Alice"});
1759/// ```
1760///
1761/// With custom message:
1762/// ```ignore
1763/// assert_json!(response, {"status": "ok"}, "API should return success status");
1764/// ```
1765#[macro_export]
1766macro_rules! assert_json {
1767    ($response:expr, $expected:tt) => {{
1768        let response = &$response;
1769        let expected = serde_json::json!($expected);
1770        let actual: serde_json::Value = response
1771            .json()
1772            .expect("Failed to parse response body as JSON");
1773
1774        if let Err(path) = $crate::testing::json_contains(&actual, &expected) {
1775            panic!(
1776                "assertion failed: JSON partial match failed at path '{}'\n\
1777                 expected (partial):\n{}\n\
1778                 actual:\n{}\n\
1779                 request id: {}",
1780                path,
1781                serde_json::to_string_pretty(&expected).unwrap_or_else(|_| format!("{:?}", expected)),
1782                serde_json::to_string_pretty(&actual).unwrap_or_else(|_| format!("{:?}", actual)),
1783                response.request_id()
1784            );
1785        }
1786    }};
1787    ($response:expr, $expected:tt, $($msg:tt)+) => {{
1788        let response = &$response;
1789        let expected = serde_json::json!($expected);
1790        let actual: serde_json::Value = response
1791            .json()
1792            .expect("Failed to parse response body as JSON");
1793
1794        if let Err(path) = $crate::testing::json_contains(&actual, &expected) {
1795            panic!(
1796                "{}\n\
1797                 assertion failed: JSON partial match failed at path '{}'\n\
1798                 expected (partial):\n{}\n\
1799                 actual:\n{}\n\
1800                 request id: {}",
1801                format_args!($($msg)+),
1802                path,
1803                serde_json::to_string_pretty(&expected).unwrap_or_else(|_| format!("{:?}", expected)),
1804                serde_json::to_string_pretty(&actual).unwrap_or_else(|_| format!("{:?}", actual)),
1805                response.request_id()
1806            );
1807        }
1808    }};
1809}
1810
1811/// Asserts that a test response body matches the given regex pattern.
1812///
1813/// Requires the `regex` feature to be enabled.
1814///
1815/// # Examples
1816///
1817/// ```ignore
1818/// use fastapi_core::assert_body_matches;
1819///
1820/// let response = client.get("/user/1").send();
1821/// assert_body_matches!(response, r"user_\d+");
1822/// assert_body_matches!(response, r"^Hello.*World$");
1823/// ```
1824#[cfg(feature = "regex")]
1825#[macro_export]
1826macro_rules! assert_body_matches {
1827    ($response:expr, $pattern:expr) => {{
1828        let response = &$response;
1829        let pattern = $pattern;
1830        let re = regex::Regex::new(pattern)
1831            .unwrap_or_else(|e| panic!("Invalid regex pattern '{}': {}", pattern, e));
1832        let body = response.text();
1833        if !re.is_match(body) {
1834            panic!(
1835                "assertion failed: response body does not match pattern\n\
1836                 pattern: \"{}\"\n\
1837                 actual body: \"{}\"\n\
1838                 request id: {}",
1839                pattern, body, response.request_id()
1840            );
1841        }
1842    }};
1843    ($response:expr, $pattern:expr, $($msg:tt)+) => {{
1844        let response = &$response;
1845        let pattern = $pattern;
1846        let re = regex::Regex::new(pattern)
1847            .unwrap_or_else(|e| panic!("Invalid regex pattern '{}': {}", pattern, e));
1848        let body = response.text();
1849        if !re.is_match(body) {
1850            panic!(
1851                "{}\n\
1852                 assertion failed: response body does not match pattern\n\
1853                 pattern: \"{}\"\n\
1854                 actual body: \"{}\"\n\
1855                 request id: {}",
1856                format_args!($($msg)+),
1857                pattern,
1858                body,
1859                response.request_id()
1860            );
1861        }
1862    }};
1863}
1864
1865// Note: json_contains is already public and accessible via crate::testing::json_contains
1866
1867// ============================================================================
1868// Lab Runtime Testing Utilities
1869// ============================================================================
1870
1871/// Configuration for Lab-based deterministic testing.
1872///
1873/// This configuration controls how the Lab runtime executes tests, including
1874/// virtual time, chaos injection, and deterministic scheduling.
1875///
1876/// # Example
1877///
1878/// ```ignore
1879/// use fastapi_core::testing::{LabTestConfig, LabTestClient};
1880///
1881/// // Basic deterministic test
1882/// let config = LabTestConfig::new(42);
1883/// let client = LabTestClient::with_config(my_handler, config);
1884///
1885/// // With chaos injection for stress testing
1886/// let config = LabTestConfig::new(42).with_light_chaos();
1887/// let client = LabTestClient::with_config(my_handler, config);
1888/// ```
1889#[derive(Debug, Clone)]
1890pub struct LabTestConfig {
1891    /// Seed for deterministic scheduling.
1892    pub seed: u64,
1893    /// Whether to enable chaos injection.
1894    pub chaos_enabled: bool,
1895    /// Chaos intensity (0.0 = none, 1.0 = max).
1896    pub chaos_intensity: f64,
1897    /// Maximum steps before timeout (prevents infinite loops).
1898    pub max_steps: Option<u64>,
1899    /// Whether to capture traces for debugging.
1900    pub capture_traces: bool,
1901}
1902
1903impl Default for LabTestConfig {
1904    fn default() -> Self {
1905        Self {
1906            seed: 42,
1907            chaos_enabled: false,
1908            chaos_intensity: 0.0,
1909            max_steps: Some(10_000),
1910            capture_traces: false,
1911        }
1912    }
1913}
1914
1915impl LabTestConfig {
1916    /// Creates a new Lab test configuration with the given seed.
1917    #[must_use]
1918    pub fn new(seed: u64) -> Self {
1919        Self {
1920            seed,
1921            ..Default::default()
1922        }
1923    }
1924
1925    /// Enables light chaos injection (1% cancel, 5% delay).
1926    ///
1927    /// Suitable for CI pipelines - catches obvious bugs without excessive flakiness.
1928    #[must_use]
1929    pub fn with_light_chaos(mut self) -> Self {
1930        self.chaos_enabled = true;
1931        self.chaos_intensity = 0.05;
1932        self
1933    }
1934
1935    /// Enables heavy chaos injection (10% cancel, 20% delay).
1936    ///
1937    /// Suitable for thorough stress testing before releases.
1938    #[must_use]
1939    pub fn with_heavy_chaos(mut self) -> Self {
1940        self.chaos_enabled = true;
1941        self.chaos_intensity = 0.2;
1942        self
1943    }
1944
1945    /// Sets custom chaos intensity (0.0 to 1.0).
1946    #[must_use]
1947    pub fn with_chaos_intensity(mut self, intensity: f64) -> Self {
1948        self.chaos_enabled = intensity > 0.0;
1949        self.chaos_intensity = intensity.clamp(0.0, 1.0);
1950        self
1951    }
1952
1953    /// Sets the maximum number of steps before timeout.
1954    #[must_use]
1955    pub fn with_max_steps(mut self, max: u64) -> Self {
1956        self.max_steps = Some(max);
1957        self
1958    }
1959
1960    /// Disables the step limit (use with caution).
1961    #[must_use]
1962    pub fn without_step_limit(mut self) -> Self {
1963        self.max_steps = None;
1964        self
1965    }
1966
1967    /// Enables trace capture for debugging.
1968    #[must_use]
1969    pub fn with_traces(mut self) -> Self {
1970        self.capture_traces = true;
1971        self
1972    }
1973}
1974
1975/// Statistics about chaos injection during a test.
1976///
1977/// This is returned by `LabTestClient::chaos_stats()` after test execution.
1978#[derive(Debug, Clone, Default)]
1979pub struct TestChaosStats {
1980    /// Number of decision points encountered.
1981    pub decision_points: u64,
1982    /// Number of delays injected.
1983    pub delays_injected: u64,
1984    /// Number of cancellations injected.
1985    pub cancellations_injected: u64,
1986    /// Total steps executed.
1987    pub steps_executed: u64,
1988}
1989
1990impl TestChaosStats {
1991    /// Returns the injection rate (injections / decision points).
1992    #[must_use]
1993    #[allow(clippy::cast_precision_loss)] // Acceptable for test stats
1994    pub fn injection_rate(&self) -> f64 {
1995        if self.decision_points == 0 {
1996            0.0
1997        } else {
1998            (self.delays_injected + self.cancellations_injected) as f64
1999                / self.decision_points as f64
2000        }
2001    }
2002
2003    /// Returns true if any chaos was injected.
2004    #[must_use]
2005    pub fn had_chaos(&self) -> bool {
2006        self.delays_injected > 0 || self.cancellations_injected > 0
2007    }
2008}
2009
2010/// Virtual time utilities for testing timeouts and delays.
2011///
2012/// This module provides helpers for simulating time passage in tests
2013/// without waiting for actual wall-clock time.
2014///
2015/// # Example
2016///
2017/// ```ignore
2018/// use fastapi_core::testing::MockTime;
2019///
2020/// let mock_time = MockTime::new();
2021///
2022/// // Advance virtual time by 5 seconds
2023/// mock_time.advance(Duration::from_secs(5));
2024///
2025/// // Check that timer has expired
2026/// assert!(mock_time.elapsed() >= Duration::from_secs(5));
2027/// ```
2028#[derive(Debug, Clone)]
2029pub struct MockTime {
2030    /// Current virtual time in microseconds.
2031    current_us: Arc<std::sync::atomic::AtomicU64>,
2032}
2033
2034impl Default for MockTime {
2035    fn default() -> Self {
2036        Self::new()
2037    }
2038}
2039
2040impl MockTime {
2041    /// Creates a new mock time starting at zero.
2042    #[must_use]
2043    pub fn new() -> Self {
2044        Self {
2045            current_us: Arc::new(std::sync::atomic::AtomicU64::new(0)),
2046        }
2047    }
2048
2049    /// Creates a mock time starting at the given duration.
2050    #[must_use]
2051    pub fn starting_at(initial: std::time::Duration) -> Self {
2052        Self {
2053            current_us: Arc::new(std::sync::atomic::AtomicU64::new(initial.as_micros() as u64)),
2054        }
2055    }
2056
2057    /// Returns the current virtual time.
2058    #[must_use]
2059    pub fn now(&self) -> std::time::Duration {
2060        std::time::Duration::from_micros(self.current_us.load(std::sync::atomic::Ordering::Relaxed))
2061    }
2062
2063    /// Returns the elapsed time since creation.
2064    #[must_use]
2065    pub fn elapsed(&self) -> std::time::Duration {
2066        self.now()
2067    }
2068
2069    /// Advances virtual time by the given duration.
2070    pub fn advance(&self, duration: std::time::Duration) {
2071        self.current_us.fetch_add(
2072            duration.as_micros() as u64,
2073            std::sync::atomic::Ordering::Relaxed,
2074        );
2075    }
2076
2077    /// Sets virtual time to a specific value.
2078    pub fn set(&self, time: std::time::Duration) {
2079        self.current_us.store(
2080            time.as_micros() as u64,
2081            std::sync::atomic::Ordering::Relaxed,
2082        );
2083    }
2084
2085    /// Resets virtual time to zero.
2086    pub fn reset(&self) {
2087        self.current_us
2088            .store(0, std::sync::atomic::Ordering::Relaxed);
2089    }
2090}
2091
2092/// Result of a cancellation test.
2093///
2094/// Contains information about how the handler responded to cancellation.
2095#[derive(Debug)]
2096pub struct CancellationTestResult {
2097    /// Whether the handler completed before cancellation.
2098    pub completed: bool,
2099    /// Whether the handler detected cancellation via checkpoint.
2100    pub cancelled_at_checkpoint: bool,
2101    /// Response returned (if handler completed).
2102    pub response: Option<Response>,
2103    /// The await point at which cancellation was detected.
2104    pub cancellation_point: Option<String>,
2105}
2106
2107impl CancellationTestResult {
2108    /// Returns true if cancellation was handled gracefully.
2109    #[must_use]
2110    pub fn gracefully_cancelled(&self) -> bool {
2111        !self.completed && self.cancelled_at_checkpoint
2112    }
2113
2114    /// Returns true if the handler completed despite cancellation request.
2115    #[must_use]
2116    pub fn completed_despite_cancel(&self) -> bool {
2117        self.completed
2118    }
2119}
2120
2121/// Helper for testing handler cancellation behavior.
2122///
2123/// # Example
2124///
2125/// ```ignore
2126/// use fastapi_core::testing::CancellationTest;
2127///
2128/// let test = CancellationTest::new(my_handler);
2129///
2130/// // Test that handler respects cancellation
2131/// let result = test.cancel_after_polls(3);
2132/// assert!(result.gracefully_cancelled());
2133/// ```
2134pub struct CancellationTest<H> {
2135    handler: H,
2136    seed: u64,
2137}
2138
2139impl<H: Handler + 'static> CancellationTest<H> {
2140    /// Creates a new cancellation test for the given handler.
2141    #[must_use]
2142    pub fn new(handler: H) -> Self {
2143        Self { handler, seed: 42 }
2144    }
2145
2146    /// Sets the seed for deterministic testing.
2147    #[must_use]
2148    pub fn with_seed(mut self, seed: u64) -> Self {
2149        self.seed = seed;
2150        self
2151    }
2152
2153    /// Tests that the handler respects cancellation via checkpoint.
2154    ///
2155    /// This sets the cancellation flag before calling the handler, then
2156    /// verifies that the handler detects it at a checkpoint and returns
2157    /// an appropriate error response.
2158    pub fn test_respects_cancellation(&self) -> CancellationTestResult {
2159        let cx = asupersync::Cx::for_testing();
2160        let ctx = RequestContext::new(cx, 1);
2161
2162        // Pre-set cancellation before handler runs
2163        ctx.cx().set_cancel_requested(true);
2164
2165        let mut request = Request::new(Method::Get, "/test");
2166        let response = futures_executor::block_on(self.handler.call(&ctx, &mut request));
2167
2168        // Check if handler returned a cancellation-related status
2169        let is_cancelled_response = response.status().as_u16() == 499
2170            || response.status().as_u16() == 504
2171            || response.status().as_u16() == 503;
2172
2173        CancellationTestResult {
2174            completed: true,
2175            cancelled_at_checkpoint: is_cancelled_response,
2176            response: Some(response),
2177            cancellation_point: None,
2178        }
2179    }
2180
2181    /// Tests that the handler completes normally without cancellation.
2182    pub fn complete_normally(&self) -> CancellationTestResult {
2183        let cx = asupersync::Cx::for_testing();
2184        let ctx = RequestContext::new(cx, 1);
2185        let mut request = Request::new(Method::Get, "/test");
2186
2187        let response = futures_executor::block_on(self.handler.call(&ctx, &mut request));
2188
2189        CancellationTestResult {
2190            completed: true,
2191            cancelled_at_checkpoint: false,
2192            response: Some(response),
2193            cancellation_point: None,
2194        }
2195    }
2196
2197    /// Tests handler behavior with a custom cancellation callback.
2198    ///
2199    /// The callback is called with the context and can decide when
2200    /// to trigger cancellation based on custom logic.
2201    pub fn test_with_cancel_callback<F>(
2202        &self,
2203        path: &str,
2204        mut cancel_fn: F,
2205    ) -> CancellationTestResult
2206    where
2207        F: FnMut(&RequestContext) -> bool,
2208    {
2209        let cx = asupersync::Cx::for_testing();
2210        let ctx = RequestContext::new(cx, 1);
2211
2212        // Call the cancel callback
2213        if cancel_fn(&ctx) {
2214            ctx.cx().set_cancel_requested(true);
2215        }
2216
2217        let mut request = Request::new(Method::Get, path);
2218        let response = futures_executor::block_on(self.handler.call(&ctx, &mut request));
2219
2220        let is_cancelled = ctx.is_cancelled();
2221        let is_cancelled_response =
2222            response.status().as_u16() == 499 || response.status().as_u16() == 504;
2223
2224        CancellationTestResult {
2225            completed: true,
2226            cancelled_at_checkpoint: is_cancelled && is_cancelled_response,
2227            response: Some(response),
2228            cancellation_point: None,
2229        }
2230    }
2231}
2232
2233#[cfg(test)]
2234mod tests {
2235    use super::*;
2236    use crate::app::App;
2237    use crate::dependency::{Depends, FromDependency};
2238    use crate::error::HttpError;
2239    use crate::extract::FromRequest;
2240    use crate::middleware::BoxFuture;
2241
2242    // Simple test handler
2243    struct EchoHandler;
2244
2245    impl Handler for EchoHandler {
2246        fn call<'a>(
2247            &'a self,
2248            _ctx: &'a RequestContext,
2249            req: &'a mut Request,
2250        ) -> BoxFuture<'a, Response> {
2251            let method = format!("{:?}", req.method());
2252            let path = req.path().to_string();
2253            let body = format!("Method: {method}, Path: {path}");
2254            Box::pin(async move {
2255                Response::ok()
2256                    .header("content-type", b"text/plain".to_vec())
2257                    .body(ResponseBody::Bytes(body.into_bytes()))
2258            })
2259        }
2260    }
2261
2262    // Handler that sets a cookie
2263    struct CookieHandler;
2264
2265    impl Handler for CookieHandler {
2266        fn call<'a>(
2267            &'a self,
2268            _ctx: &'a RequestContext,
2269            _req: &'a mut Request,
2270        ) -> BoxFuture<'a, Response> {
2271            Box::pin(async move {
2272                Response::ok()
2273                    .header("set-cookie", b"session=abc123".to_vec())
2274                    .body(ResponseBody::Bytes(b"Cookie set".to_vec()))
2275            })
2276        }
2277    }
2278
2279    // Handler that echoes the cookie
2280    struct CookieEchoHandler;
2281
2282    impl Handler for CookieEchoHandler {
2283        fn call<'a>(
2284            &'a self,
2285            _ctx: &'a RequestContext,
2286            req: &'a mut Request,
2287        ) -> BoxFuture<'a, Response> {
2288            let cookie = req.headers().get("cookie").map_or_else(
2289                || "no cookies".to_string(),
2290                |v| String::from_utf8_lossy(v).to_string(),
2291            );
2292            Box::pin(async move { Response::ok().body(ResponseBody::Bytes(cookie.into_bytes())) })
2293        }
2294    }
2295
2296    #[derive(Clone)]
2297    struct OverrideDep {
2298        value: usize,
2299    }
2300
2301    impl FromDependency for OverrideDep {
2302        type Error = HttpError;
2303
2304        async fn from_dependency(
2305            _ctx: &RequestContext,
2306            _req: &mut Request,
2307        ) -> Result<Self, Self::Error> {
2308            Ok(Self { value: 1 })
2309        }
2310    }
2311
2312    struct OverrideDepHandler;
2313
2314    impl Handler for OverrideDepHandler {
2315        fn call<'a>(
2316            &'a self,
2317            ctx: &'a RequestContext,
2318            req: &'a mut Request,
2319        ) -> BoxFuture<'a, Response> {
2320            Box::pin(async move {
2321                let dep = Depends::<OverrideDep>::from_request(ctx, req)
2322                    .await
2323                    .expect("dependency extraction failed");
2324                Response::ok().body(ResponseBody::Bytes(dep.value.to_string().into_bytes()))
2325            })
2326        }
2327    }
2328
2329    fn override_dep_route(ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
2330        let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(ctx, req))
2331            .expect("dependency extraction failed");
2332        std::future::ready(
2333            Response::ok().body(ResponseBody::Bytes(dep.value.to_string().into_bytes())),
2334        )
2335    }
2336
2337    #[test]
2338    fn test_client_get() {
2339        let client = TestClient::new(EchoHandler);
2340        let response = client.get("/test/path").send();
2341
2342        assert_eq!(response.status_code(), 200);
2343        assert_eq!(response.text(), "Method: Get, Path: /test/path");
2344    }
2345
2346    #[test]
2347    fn test_client_post() {
2348        let client = TestClient::new(EchoHandler);
2349        let response = client.post("/api/items").send();
2350
2351        assert_eq!(response.status_code(), 200);
2352        assert!(response.text().contains("Method: Post"));
2353    }
2354
2355    // Note: This test is ignored because override_dep_route uses block_on internally,
2356    // which causes nested executor issues when TestClient::execute also uses block_on.
2357    // The same functionality is tested by test_test_client_override_clear using
2358    // the OverrideDepHandler struct which properly uses async/await.
2359    #[test]
2360    #[ignore = "nested executor issue: override_dep_route uses block_on inside TestClient's block_on"]
2361    fn test_app_dependency_override_used_by_test_client() {
2362        let app = App::builder()
2363            .route("/", Method::Get, override_dep_route)
2364            .build();
2365
2366        app.override_dependency_value(OverrideDep { value: 42 });
2367
2368        let client = TestClient::new(app);
2369
2370        let response = client.get("/").send();
2371
2372        assert_eq!(response.text(), "42");
2373    }
2374
2375    #[test]
2376    fn test_test_client_override_clear() {
2377        let client = TestClient::new(OverrideDepHandler);
2378
2379        client.override_dependency_value(OverrideDep { value: 9 });
2380        let response = client.get("/").send();
2381        assert_eq!(response.text(), "9");
2382
2383        client.clear_dependency_overrides();
2384        let response = client.get("/").send();
2385        assert_eq!(response.text(), "1");
2386    }
2387
2388    #[test]
2389    fn test_client_all_methods() {
2390        let client = TestClient::new(EchoHandler);
2391
2392        assert!(client.get("/").send().text().contains("Get"));
2393        assert!(client.post("/").send().text().contains("Post"));
2394        assert!(client.put("/").send().text().contains("Put"));
2395        assert!(client.delete("/").send().text().contains("Delete"));
2396        assert!(client.patch("/").send().text().contains("Patch"));
2397        assert!(client.options("/").send().text().contains("Options"));
2398        assert!(client.head("/").send().text().contains("Head"));
2399    }
2400
2401    #[test]
2402    fn test_query_params() {
2403        let client = TestClient::new(EchoHandler);
2404        let response = client
2405            .get("/search")
2406            .query("q", "rust")
2407            .query("limit", "10")
2408            .send();
2409
2410        assert_eq!(response.status_code(), 200);
2411    }
2412
2413    #[test]
2414    fn test_response_assertions() {
2415        let client = TestClient::new(EchoHandler);
2416        let response = client.get("/test").send();
2417
2418        let _ = response
2419            .assert_status_code(200)
2420            .assert_success()
2421            .assert_header("content-type", "text/plain")
2422            .assert_text_contains("Get");
2423    }
2424
2425    #[test]
2426    fn test_response_status_checks() {
2427        let client = TestClient::new(EchoHandler);
2428        let response = client.get("/").send();
2429
2430        assert!(response.is_success());
2431        assert!(!response.is_redirect());
2432        assert!(!response.is_client_error());
2433        assert!(!response.is_server_error());
2434    }
2435
2436    #[test]
2437    fn test_cookie_jar() {
2438        let mut jar = CookieJar::new();
2439        assert!(jar.is_empty());
2440
2441        jar.set("session", "abc123");
2442        jar.set("user", "alice");
2443
2444        assert_eq!(jar.len(), 2);
2445        assert_eq!(jar.get("session"), Some("abc123"));
2446        assert_eq!(jar.get("user"), Some("alice"));
2447
2448        let header = jar.to_cookie_header().unwrap();
2449        assert!(header.contains("session=abc123"));
2450        assert!(header.contains("user=alice"));
2451
2452        jar.remove("session");
2453        assert_eq!(jar.len(), 1);
2454        assert_eq!(jar.get("session"), None);
2455    }
2456
2457    #[test]
2458    fn test_cookie_jar_request_matching_rules() {
2459        use crate::request::ConnectionInfo;
2460
2461        let mut jar = CookieJar::new();
2462
2463        let mut req = Request::new(Method::Get, "/account/settings");
2464        req.headers_mut().insert("host", b"example.com".to_vec());
2465
2466        // Secure cookie should not be sent over non-secure request.
2467        jar.parse_set_cookie(
2468            &req,
2469            b"sid=1; Path=/account; Secure; HttpOnly; SameSite=Lax",
2470        );
2471        assert_eq!(jar.cookie_header_for_request(&req), None);
2472
2473        // Mark request as TLS-enabled; now it matches.
2474        req.insert_extension(ConnectionInfo::HTTPS);
2475        assert_eq!(
2476            jar.cookie_header_for_request(&req).as_deref(),
2477            Some("sid=1")
2478        );
2479
2480        // Path mismatch should prevent sending.
2481        let mut req2 = Request::new(Method::Get, "/other");
2482        req2.headers_mut().insert("host", b"example.com".to_vec());
2483        req2.insert_extension(ConnectionInfo::HTTPS);
2484        assert_eq!(jar.cookie_header_for_request(&req2), None);
2485
2486        // Domain cookies should match subdomains.
2487        jar.parse_set_cookie(&req, b"sub=1; Domain=example.com; Path=/");
2488        let mut req3 = Request::new(Method::Get, "/");
2489        req3.headers_mut()
2490            .insert("host", b"api.example.com".to_vec());
2491        let hdr = jar.cookie_header_for_request(&req3).expect("cookie header");
2492        assert!(hdr.contains("sub=1"));
2493    }
2494
2495    #[test]
2496    fn test_cookie_persistence() {
2497        let client = TestClient::new(CookieHandler);
2498
2499        // First request sets a cookie
2500        let _ = client.get("/set-cookie").send();
2501
2502        // Cookie should be in the jar
2503        assert_eq!(client.cookies().get("session"), Some("abc123"));
2504
2505        // Use a new handler that echoes cookies
2506        let client2 = TestClient::new(CookieEchoHandler);
2507        client2.cookies().set("session", "abc123");
2508
2509        let response = client2.get("/check-cookie").send();
2510        assert!(response.text().contains("session=abc123"));
2511    }
2512
2513    #[test]
2514    fn test_request_id_increments() {
2515        let client = TestClient::new(EchoHandler);
2516
2517        let r1 = client.get("/").send();
2518        let r2 = client.get("/").send();
2519        let r3 = client.get("/").send();
2520
2521        assert_eq!(r1.request_id(), 1);
2522        assert_eq!(r2.request_id(), 2);
2523        assert_eq!(r3.request_id(), 3);
2524    }
2525
2526    #[test]
2527    fn test_client_with_seed() {
2528        let client = TestClient::with_seed(EchoHandler, 42);
2529        assert_eq!(client.seed(), Some(42));
2530    }
2531
2532    #[test]
2533    fn test_client_clone() {
2534        let client = TestClient::new(EchoHandler);
2535        client.cookies().set("test", "value");
2536
2537        let cloned = client.clone();
2538
2539        // Cloned client shares cookies
2540        assert_eq!(cloned.cookies().get("test"), Some("value"));
2541
2542        // And request ID counter
2543        let r1 = client.get("/").send();
2544        let r2 = cloned.get("/").send();
2545        assert_eq!(r1.request_id(), 1);
2546        assert_eq!(r2.request_id(), 2);
2547    }
2548
2549    // =========================================================================
2550    // Tests for json_contains partial matching
2551    // =========================================================================
2552
2553    #[test]
2554    fn test_json_contains_exact_match() {
2555        let actual = serde_json::json!({"id": 1, "name": "Alice"});
2556        let expected = serde_json::json!({"id": 1, "name": "Alice"});
2557        assert!(json_contains(&actual, &expected).is_ok());
2558    }
2559
2560    #[test]
2561    fn test_json_contains_partial_match() {
2562        let actual = serde_json::json!({"id": 1, "name": "Alice", "email": "alice@example.com"});
2563        let expected = serde_json::json!({"name": "Alice"});
2564        assert!(json_contains(&actual, &expected).is_ok());
2565    }
2566
2567    #[test]
2568    fn test_json_contains_nested_partial_match() {
2569        let actual = serde_json::json!({
2570            "user": {"id": 1, "name": "Alice", "email": "alice@example.com"},
2571            "status": "active"
2572        });
2573        let expected = serde_json::json!({
2574            "user": {"name": "Alice"}
2575        });
2576        assert!(json_contains(&actual, &expected).is_ok());
2577    }
2578
2579    #[test]
2580    fn test_json_contains_mismatch_value() {
2581        let actual = serde_json::json!({"id": 1, "name": "Alice"});
2582        let expected = serde_json::json!({"name": "Bob"});
2583        let result = json_contains(&actual, &expected);
2584        assert!(result.is_err());
2585        assert_eq!(result.unwrap_err(), "$.name");
2586    }
2587
2588    #[test]
2589    fn test_json_contains_missing_key() {
2590        let actual = serde_json::json!({"id": 1, "name": "Alice"});
2591        let expected = serde_json::json!({"email": "alice@example.com"});
2592        let result = json_contains(&actual, &expected);
2593        assert!(result.is_err());
2594        assert_eq!(result.unwrap_err(), "$.email");
2595    }
2596
2597    #[test]
2598    fn test_json_contains_array_exact_match() {
2599        let actual = serde_json::json!({"items": [1, 2, 3]});
2600        let expected = serde_json::json!({"items": [1, 2, 3]});
2601        assert!(json_contains(&actual, &expected).is_ok());
2602    }
2603
2604    #[test]
2605    fn test_json_contains_array_length_mismatch() {
2606        let actual = serde_json::json!({"items": [1, 2, 3]});
2607        let expected = serde_json::json!({"items": [1, 2]});
2608        let result = json_contains(&actual, &expected);
2609        assert!(result.is_err());
2610        assert_eq!(result.unwrap_err(), "$.items[length]");
2611    }
2612
2613    #[test]
2614    fn test_json_contains_array_element_mismatch() {
2615        let actual = serde_json::json!({"items": [1, 2, 3]});
2616        let expected = serde_json::json!({"items": [1, 5, 3]});
2617        let result = json_contains(&actual, &expected);
2618        assert!(result.is_err());
2619        assert_eq!(result.unwrap_err(), "$.items[1]");
2620    }
2621
2622    #[test]
2623    fn test_json_contains_primitives() {
2624        // Numbers
2625        assert!(json_contains(&serde_json::json!(42), &serde_json::json!(42)).is_ok());
2626        assert!(json_contains(&serde_json::json!(42), &serde_json::json!(43)).is_err());
2627
2628        // Strings
2629        assert!(json_contains(&serde_json::json!("hello"), &serde_json::json!("hello")).is_ok());
2630        assert!(json_contains(&serde_json::json!("hello"), &serde_json::json!("world")).is_err());
2631
2632        // Booleans
2633        assert!(json_contains(&serde_json::json!(true), &serde_json::json!(true)).is_ok());
2634        assert!(json_contains(&serde_json::json!(true), &serde_json::json!(false)).is_err());
2635
2636        // Null
2637        assert!(json_contains(&serde_json::json!(null), &serde_json::json!(null)).is_ok());
2638    }
2639
2640    #[test]
2641    fn test_json_contains_type_mismatch() {
2642        let actual = serde_json::json!({"id": "1"});
2643        let expected = serde_json::json!({"id": 1});
2644        let result = json_contains(&actual, &expected);
2645        assert!(result.is_err());
2646        assert_eq!(result.unwrap_err(), "$.id");
2647    }
2648
2649    #[test]
2650    fn test_json_contains_deeply_nested() {
2651        let actual = serde_json::json!({
2652            "level1": {
2653                "level2": {
2654                    "level3": {
2655                        "value": 42,
2656                        "extra": "ignored"
2657                    }
2658                }
2659            }
2660        });
2661        let expected = serde_json::json!({
2662            "level1": {
2663                "level2": {
2664                    "level3": {
2665                        "value": 42
2666                    }
2667                }
2668            }
2669        });
2670        assert!(json_contains(&actual, &expected).is_ok());
2671    }
2672
2673    // =========================================================================
2674    // Tests for assertion macros using handler that returns JSON
2675    // =========================================================================
2676
2677    // Handler that returns JSON
2678    struct JsonHandler;
2679
2680    impl Handler for JsonHandler {
2681        fn call<'a>(
2682            &'a self,
2683            _ctx: &'a RequestContext,
2684            _req: &'a mut Request,
2685        ) -> BoxFuture<'a, Response> {
2686            let json = serde_json::json!({
2687                "id": 1,
2688                "name": "Alice",
2689                "email": "alice@example.com",
2690                "active": true
2691            });
2692            let body = serde_json::to_vec(&json).unwrap();
2693            Box::pin(async move {
2694                Response::ok()
2695                    .header("content-type", b"application/json".to_vec())
2696                    .header("x-request-id", b"req-123".to_vec())
2697                    .body(ResponseBody::Bytes(body))
2698            })
2699        }
2700    }
2701
2702    // Handler that returns a specific status code
2703    #[allow(dead_code)]
2704    struct StatusHandler(u16);
2705
2706    #[allow(dead_code)]
2707    impl Handler for StatusHandler {
2708        fn call<'a>(
2709            &'a self,
2710            _ctx: &'a RequestContext,
2711            _req: &'a mut Request,
2712        ) -> BoxFuture<'a, Response> {
2713            let status = StatusCode::from_u16(self.0);
2714            Box::pin(async move { Response::with_status(status) })
2715        }
2716    }
2717
2718    #[test]
2719    fn test_assert_status_macro_with_u16() {
2720        let client = TestClient::new(EchoHandler);
2721        let response = client.get("/").send();
2722        crate::assert_status!(response, 200);
2723    }
2724
2725    #[test]
2726    fn test_assert_status_macro_with_status_code() {
2727        let client = TestClient::new(EchoHandler);
2728        let response = client.get("/").send();
2729        crate::assert_status!(response, StatusCode::OK);
2730    }
2731
2732    #[test]
2733    #[should_panic(expected = "assertion failed")]
2734    fn test_assert_status_macro_failure() {
2735        let client = TestClient::new(EchoHandler);
2736        let response = client.get("/").send();
2737        crate::assert_status!(response, 404);
2738    }
2739
2740    #[test]
2741    fn test_assert_header_macro() {
2742        let client = TestClient::new(EchoHandler);
2743        let response = client.get("/").send();
2744        crate::assert_header!(response, "content-type", "text/plain");
2745    }
2746
2747    #[test]
2748    #[should_panic(expected = "assertion failed")]
2749    fn test_assert_header_macro_failure() {
2750        let client = TestClient::new(EchoHandler);
2751        let response = client.get("/").send();
2752        crate::assert_header!(response, "content-type", "application/json");
2753    }
2754
2755    #[test]
2756    fn test_assert_body_contains_macro() {
2757        let client = TestClient::new(EchoHandler);
2758        let response = client.get("/test").send();
2759        crate::assert_body_contains!(response, "Method: Get");
2760        crate::assert_body_contains!(response, "Path: /test");
2761    }
2762
2763    #[test]
2764    #[should_panic(expected = "assertion failed")]
2765    fn test_assert_body_contains_macro_failure() {
2766        let client = TestClient::new(EchoHandler);
2767        let response = client.get("/test").send();
2768        crate::assert_body_contains!(response, "nonexistent");
2769    }
2770
2771    #[test]
2772    fn test_assert_json_macro_partial_match() {
2773        let client = TestClient::new(JsonHandler);
2774        let response = client.get("/user").send();
2775
2776        // Partial match - only check some fields
2777        crate::assert_json!(response, {"name": "Alice"});
2778        crate::assert_json!(response, {"id": 1, "active": true});
2779    }
2780
2781    #[test]
2782    fn test_assert_json_macro_exact_match() {
2783        let client = TestClient::new(JsonHandler);
2784        let response = client.get("/user").send();
2785
2786        // Exact match - all fields
2787        crate::assert_json!(response, {
2788            "id": 1,
2789            "name": "Alice",
2790            "email": "alice@example.com",
2791            "active": true
2792        });
2793    }
2794
2795    #[test]
2796    #[should_panic(expected = "JSON partial match failed")]
2797    fn test_assert_json_macro_failure() {
2798        let client = TestClient::new(JsonHandler);
2799        let response = client.get("/user").send();
2800        crate::assert_json!(response, {"name": "Bob"});
2801    }
2802
2803    // =========================================================================
2804    // Tests for method-based assertions
2805    // =========================================================================
2806
2807    #[test]
2808    fn test_assert_json_contains_method() {
2809        let client = TestClient::new(JsonHandler);
2810        let response = client.get("/user").send();
2811
2812        let _ = response.assert_json_contains(&serde_json::json!({"name": "Alice"}));
2813    }
2814
2815    #[test]
2816    fn test_assert_header_exists() {
2817        let client = TestClient::new(JsonHandler);
2818        let response = client.get("/").send();
2819
2820        let _ = response
2821            .assert_header_exists("content-type")
2822            .assert_header_exists("x-request-id");
2823    }
2824
2825    #[test]
2826    #[should_panic(expected = "Expected header 'nonexistent' to exist")]
2827    fn test_assert_header_exists_failure() {
2828        let client = TestClient::new(JsonHandler);
2829        let response = client.get("/").send();
2830        let _ = response.assert_header_exists("nonexistent");
2831    }
2832
2833    #[test]
2834    fn test_assert_header_missing() {
2835        let client = TestClient::new(JsonHandler);
2836        let response = client.get("/").send();
2837
2838        let _ = response.assert_header_missing("x-nonexistent");
2839    }
2840
2841    #[test]
2842    #[should_panic(expected = "Expected header 'content-type' to not exist")]
2843    fn test_assert_header_missing_failure() {
2844        let client = TestClient::new(JsonHandler);
2845        let response = client.get("/").send();
2846        let _ = response.assert_header_missing("content-type");
2847    }
2848
2849    #[test]
2850    fn test_assert_content_type_contains() {
2851        let client = TestClient::new(JsonHandler);
2852        let response = client.get("/").send();
2853
2854        let _ = response.assert_content_type_contains("application/json");
2855        let _ = response.assert_content_type_contains("json");
2856    }
2857
2858    #[test]
2859    #[should_panic(expected = "Expected Content-Type to contain")]
2860    fn test_assert_content_type_contains_failure() {
2861        let client = TestClient::new(JsonHandler);
2862        let response = client.get("/").send();
2863        let _ = response.assert_content_type_contains("text/html");
2864    }
2865
2866    #[test]
2867    fn test_assertion_chaining() {
2868        let client = TestClient::new(JsonHandler);
2869        let response = client.get("/user").send();
2870
2871        // All assertions can be chained
2872        let _ = response
2873            .assert_status_code(200)
2874            .assert_success()
2875            .assert_header_exists("content-type")
2876            .assert_content_type_contains("json")
2877            .assert_json_contains(&serde_json::json!({"name": "Alice"}));
2878    }
2879
2880    #[test]
2881    fn test_macro_with_custom_message() {
2882        let client = TestClient::new(EchoHandler);
2883        let response = client.get("/").send();
2884
2885        // These should pass (custom message only shown on failure)
2886        crate::assert_status!(response, 200, "Expected 200 OK from echo handler");
2887        crate::assert_header!(
2888            response,
2889            "content-type",
2890            "text/plain",
2891            "Should have text content type"
2892        );
2893        crate::assert_body_contains!(response, "Get", "Should contain HTTP method");
2894    }
2895
2896    // =========================================================================
2897    // DI Integration Tests (fastapi_rust-zf4)
2898    // =========================================================================
2899
2900    // Test complex nested dependency graph with App and TestClient
2901    #[derive(Clone)]
2902    struct DatabasePool {
2903        connection_string: String,
2904    }
2905
2906    impl FromDependency for DatabasePool {
2907        type Error = HttpError;
2908        async fn from_dependency(
2909            _ctx: &RequestContext,
2910            _req: &mut Request,
2911        ) -> Result<Self, Self::Error> {
2912            Ok(DatabasePool {
2913                connection_string: "postgres://localhost/test".to_string(),
2914            })
2915        }
2916    }
2917
2918    #[derive(Clone)]
2919    struct UserRepository {
2920        pool_conn_str: String,
2921    }
2922
2923    impl FromDependency for UserRepository {
2924        type Error = HttpError;
2925        async fn from_dependency(
2926            ctx: &RequestContext,
2927            req: &mut Request,
2928        ) -> Result<Self, Self::Error> {
2929            let pool = Depends::<DatabasePool>::from_request(ctx, req).await?;
2930            Ok(UserRepository {
2931                pool_conn_str: pool.connection_string.clone(),
2932            })
2933        }
2934    }
2935
2936    #[derive(Clone)]
2937    struct AuthService {
2938        user_repo_pool: String,
2939    }
2940
2941    impl FromDependency for AuthService {
2942        type Error = HttpError;
2943        async fn from_dependency(
2944            ctx: &RequestContext,
2945            req: &mut Request,
2946        ) -> Result<Self, Self::Error> {
2947            let repo = Depends::<UserRepository>::from_request(ctx, req).await?;
2948            Ok(AuthService {
2949                user_repo_pool: repo.pool_conn_str.clone(),
2950            })
2951        }
2952    }
2953
2954    struct ComplexDepHandler;
2955
2956    impl Handler for ComplexDepHandler {
2957        fn call<'a>(
2958            &'a self,
2959            ctx: &'a RequestContext,
2960            req: &'a mut Request,
2961        ) -> BoxFuture<'a, Response> {
2962            Box::pin(async move {
2963                let auth = Depends::<AuthService>::from_request(ctx, req)
2964                    .await
2965                    .expect("dependency resolution failed");
2966                let body = format!("AuthService.pool={}", auth.user_repo_pool);
2967                Response::ok().body(ResponseBody::Bytes(body.into_bytes()))
2968            })
2969        }
2970    }
2971
2972    #[test]
2973    fn test_full_request_with_complex_deps() {
2974        // Test a realistic handler with nested dependencies:
2975        // Handler -> AuthService -> UserRepository -> DatabasePool
2976        let client = TestClient::new(ComplexDepHandler);
2977        let response = client.get("/auth/check").send();
2978
2979        assert_eq!(response.status_code(), 200);
2980        assert!(response.text().contains("postgres://localhost/test"));
2981    }
2982
2983    #[test]
2984    fn test_complex_deps_with_override_at_leaf() {
2985        // Override the leaf dependency (DatabasePool) and verify it propagates
2986        let client = TestClient::new(ComplexDepHandler);
2987        client.override_dependency_value(DatabasePool {
2988            connection_string: "mysql://prod/users".to_string(),
2989        });
2990
2991        let response = client.get("/auth/check").send();
2992
2993        assert_eq!(response.status_code(), 200);
2994        assert!(
2995            response.text().contains("mysql://prod/users"),
2996            "Override at leaf should propagate through dependency chain"
2997        );
2998    }
2999
3000    #[test]
3001    fn test_complex_deps_with_override_at_middle() {
3002        // Override the middle dependency (UserRepository)
3003        let client = TestClient::new(ComplexDepHandler);
3004        client.override_dependency_value(UserRepository {
3005            pool_conn_str: "overridden-repo-connection".to_string(),
3006        });
3007
3008        let response = client.get("/auth/check").send();
3009
3010        assert_eq!(response.status_code(), 200);
3011        assert!(
3012            response.text().contains("overridden-repo-connection"),
3013            "Override at middle level should be used"
3014        );
3015    }
3016
3017    #[test]
3018    fn test_dependency_caching_across_handler() {
3019        // Test that dependencies are cached within a single request
3020        use std::sync::atomic::{AtomicUsize, Ordering};
3021
3022        static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
3023
3024        #[derive(Clone)]
3025        struct TrackedDep {
3026            call_number: usize,
3027        }
3028
3029        impl FromDependency for TrackedDep {
3030            type Error = HttpError;
3031            async fn from_dependency(
3032                _ctx: &RequestContext,
3033                _req: &mut Request,
3034            ) -> Result<Self, Self::Error> {
3035                let call_number = CALL_COUNT.fetch_add(1, Ordering::SeqCst);
3036                Ok(TrackedDep { call_number })
3037            }
3038        }
3039
3040        struct MultiDepHandler;
3041
3042        impl Handler for MultiDepHandler {
3043            fn call<'a>(
3044                &'a self,
3045                ctx: &'a RequestContext,
3046                req: &'a mut Request,
3047            ) -> BoxFuture<'a, Response> {
3048                Box::pin(async move {
3049                    // Request the same dependency twice
3050                    let dep1 = Depends::<TrackedDep>::from_request(ctx, req)
3051                        .await
3052                        .expect("first resolution failed");
3053                    let dep2 = Depends::<TrackedDep>::from_request(ctx, req)
3054                        .await
3055                        .expect("second resolution failed");
3056
3057                    // Both should be the same (cached)
3058                    let body = format!("dep1={} dep2={}", dep1.call_number, dep2.call_number);
3059                    Response::ok().body(ResponseBody::Bytes(body.into_bytes()))
3060                })
3061            }
3062        }
3063
3064        // Reset counter
3065        CALL_COUNT.store(0, Ordering::SeqCst);
3066
3067        let client = TestClient::new(MultiDepHandler);
3068        let response = client.get("/").send();
3069
3070        let text = response.text();
3071        // Both deps should have the same call number (cached)
3072        assert!(
3073            text.contains("dep1=0 dep2=0"),
3074            "Dependencies should be cached within request. Got: {}",
3075            text
3076        );
3077
3078        // Counter should only have been incremented once
3079        assert_eq!(CALL_COUNT.load(Ordering::SeqCst), 1);
3080    }
3081
3082    // =========================================================================
3083    // Lab Runtime Testing Utilities Tests
3084    // =========================================================================
3085
3086    #[test]
3087    #[allow(clippy::float_cmp)] // Comparing exact literals is safe
3088    fn lab_test_config_defaults() {
3089        let config = LabTestConfig::default();
3090        assert_eq!(config.seed, 42);
3091        assert!(!config.chaos_enabled);
3092        assert_eq!(config.chaos_intensity, 0.0);
3093        assert_eq!(config.max_steps, Some(10_000));
3094        assert!(!config.capture_traces);
3095    }
3096
3097    #[test]
3098    fn lab_test_config_with_seed() {
3099        let config = LabTestConfig::new(12345);
3100        assert_eq!(config.seed, 12345);
3101    }
3102
3103    #[test]
3104    #[allow(clippy::float_cmp)] // Comparing exact literals is safe
3105    fn lab_test_config_light_chaos() {
3106        let config = LabTestConfig::new(42).with_light_chaos();
3107        assert!(config.chaos_enabled);
3108        assert_eq!(config.chaos_intensity, 0.05);
3109    }
3110
3111    #[test]
3112    #[allow(clippy::float_cmp)] // Comparing exact literals is safe
3113    fn lab_test_config_heavy_chaos() {
3114        let config = LabTestConfig::new(42).with_heavy_chaos();
3115        assert!(config.chaos_enabled);
3116        assert_eq!(config.chaos_intensity, 0.2);
3117    }
3118
3119    #[test]
3120    #[allow(clippy::float_cmp)] // Comparing exact literals is safe
3121    fn lab_test_config_custom_intensity() {
3122        let config = LabTestConfig::new(42).with_chaos_intensity(0.15);
3123        assert!(config.chaos_enabled);
3124        assert_eq!(config.chaos_intensity, 0.15);
3125    }
3126
3127    #[test]
3128    #[allow(clippy::float_cmp)] // Comparing exact literals is safe
3129    fn lab_test_config_intensity_clamps() {
3130        let config = LabTestConfig::new(42).with_chaos_intensity(1.5);
3131        assert_eq!(config.chaos_intensity, 1.0);
3132
3133        let config = LabTestConfig::new(42).with_chaos_intensity(-0.5);
3134        assert_eq!(config.chaos_intensity, 0.0);
3135        assert!(!config.chaos_enabled);
3136    }
3137
3138    #[test]
3139    fn lab_test_config_max_steps() {
3140        let config = LabTestConfig::new(42).with_max_steps(1000);
3141        assert_eq!(config.max_steps, Some(1000));
3142    }
3143
3144    #[test]
3145    fn lab_test_config_no_step_limit() {
3146        let config = LabTestConfig::new(42).without_step_limit();
3147        assert_eq!(config.max_steps, None);
3148    }
3149
3150    #[test]
3151    fn lab_test_config_with_traces() {
3152        let config = LabTestConfig::new(42).with_traces();
3153        assert!(config.capture_traces);
3154    }
3155
3156    #[test]
3157    #[allow(clippy::float_cmp)] // Comparing exact 0.0 is safe
3158    fn test_chaos_stats_empty() {
3159        let stats = TestChaosStats::default();
3160        assert_eq!(stats.decision_points, 0);
3161        assert_eq!(stats.delays_injected, 0);
3162        assert_eq!(stats.cancellations_injected, 0);
3163        assert_eq!(stats.injection_rate(), 0.0);
3164        assert!(!stats.had_chaos());
3165    }
3166
3167    #[test]
3168    fn test_chaos_stats_with_injections() {
3169        let stats = TestChaosStats {
3170            decision_points: 100,
3171            delays_injected: 5,
3172            cancellations_injected: 2,
3173            steps_executed: 50,
3174        };
3175        assert!((stats.injection_rate() - 0.07).abs() < 0.001);
3176        assert!(stats.had_chaos());
3177    }
3178
3179    #[test]
3180    fn mock_time_basic() {
3181        let time = MockTime::new();
3182        assert_eq!(time.now(), std::time::Duration::ZERO);
3183        assert_eq!(time.elapsed(), std::time::Duration::ZERO);
3184
3185        time.advance(std::time::Duration::from_secs(5));
3186        assert_eq!(time.now(), std::time::Duration::from_secs(5));
3187    }
3188
3189    #[test]
3190    fn mock_time_set_and_reset() {
3191        let time = MockTime::new();
3192        time.set(std::time::Duration::from_secs(100));
3193        assert_eq!(time.now(), std::time::Duration::from_secs(100));
3194
3195        time.reset();
3196        assert_eq!(time.now(), std::time::Duration::ZERO);
3197    }
3198
3199    #[test]
3200    fn mock_time_starting_at() {
3201        let time = MockTime::starting_at(std::time::Duration::from_secs(10));
3202        assert_eq!(time.now(), std::time::Duration::from_secs(10));
3203    }
3204
3205    #[test]
3206    fn cancellation_test_completes_normally() {
3207        let test = CancellationTest::new(EchoHandler);
3208        let result = test.complete_normally();
3209
3210        assert!(result.completed);
3211        assert!(!result.cancelled_at_checkpoint);
3212        assert!(result.response.is_some());
3213        assert_eq!(result.response.as_ref().unwrap().status().as_u16(), 200);
3214    }
3215
3216    #[test]
3217    fn cancellation_test_respects_cancellation() {
3218        // Handler that checks cancellation via checkpoint
3219        struct CheckpointHandler;
3220
3221        impl Handler for CheckpointHandler {
3222            fn call<'a>(
3223                &'a self,
3224                ctx: &'a RequestContext,
3225                _req: &'a mut Request,
3226            ) -> BoxFuture<'a, Response> {
3227                Box::pin(async move {
3228                    // Check for cancellation
3229                    if ctx.checkpoint().is_err() {
3230                        return Response::with_status(StatusCode::CLIENT_CLOSED_REQUEST);
3231                    }
3232                    Response::ok().body(ResponseBody::Bytes(b"OK".to_vec()))
3233                })
3234            }
3235        }
3236
3237        let test = CancellationTest::new(CheckpointHandler);
3238        let result = test.test_respects_cancellation();
3239
3240        assert!(result.completed);
3241        assert!(result.cancelled_at_checkpoint);
3242        assert!(result.response.is_some());
3243        // Should return 499 (CLIENT_CLOSED_REQUEST) when cancelled
3244        assert_eq!(result.response.as_ref().unwrap().status().as_u16(), 499);
3245    }
3246
3247    #[test]
3248    fn cancellation_test_result_helpers() {
3249        let graceful = CancellationTestResult {
3250            completed: false,
3251            cancelled_at_checkpoint: true,
3252            response: None,
3253            cancellation_point: None,
3254        };
3255        assert!(graceful.gracefully_cancelled());
3256        assert!(!graceful.completed_despite_cancel());
3257
3258        let completed = CancellationTestResult {
3259            completed: true,
3260            cancelled_at_checkpoint: false,
3261            response: Some(Response::ok()),
3262            cancellation_point: None,
3263        };
3264        assert!(!completed.gracefully_cancelled());
3265        assert!(completed.completed_despite_cancel());
3266    }
3267
3268    // =========================================================================
3269    // Tests for TestLogger and LogCapture (bd-2of7)
3270    // =========================================================================
3271
3272    #[test]
3273    fn test_logger_captures_all_levels() {
3274        let logger = TestLogger::new();
3275
3276        logger.log_message(LogLevel::Debug, "debug message", 1);
3277        logger.log_message(LogLevel::Info, "info message", 1);
3278        logger.log_message(LogLevel::Warn, "warn message", 1);
3279        logger.log_message(LogLevel::Error, "error message", 1);
3280
3281        let logs = logger.logs();
3282        assert_eq!(logs.len(), 4);
3283
3284        assert_eq!(logs[0].level, LogLevel::Debug);
3285        assert_eq!(logs[1].level, LogLevel::Info);
3286        assert_eq!(logs[2].level, LogLevel::Warn);
3287        assert_eq!(logs[3].level, LogLevel::Error);
3288    }
3289
3290    #[test]
3291    fn test_logger_logs_at_level_filters_correctly() {
3292        let logger = TestLogger::new();
3293
3294        logger.log_message(LogLevel::Debug, "debug", 1);
3295        logger.log_message(LogLevel::Info, "info 1", 1);
3296        logger.log_message(LogLevel::Info, "info 2", 2);
3297        logger.log_message(LogLevel::Warn, "warn", 1);
3298        logger.log_message(LogLevel::Error, "error", 1);
3299
3300        let info_logs = logger.logs_at_level(LogLevel::Info);
3301        assert_eq!(info_logs.len(), 2);
3302        assert!(info_logs[0].contains("info 1"));
3303        assert!(info_logs[1].contains("info 2"));
3304
3305        let error_logs = logger.logs_at_level(LogLevel::Error);
3306        assert_eq!(error_logs.len(), 1);
3307        assert!(error_logs[0].contains("error"));
3308
3309        let trace_logs = logger.logs_at_level(LogLevel::Trace);
3310        assert_eq!(trace_logs.len(), 0);
3311    }
3312
3313    #[test]
3314    fn test_logger_contains_message_search() {
3315        let logger = TestLogger::new();
3316
3317        logger.log_message(LogLevel::Info, "User alice logged in", 100);
3318        logger.log_message(LogLevel::Info, "Request processed for /api/users", 101);
3319        logger.log_message(LogLevel::Warn, "Rate limit approaching for alice", 102);
3320
3321        assert!(logger.contains_message("alice"));
3322        assert!(logger.contains_message("/api/users"));
3323        assert!(logger.contains_message("Rate limit"));
3324        assert!(!logger.contains_message("bob"));
3325        assert!(!logger.contains_message("nonexistent"));
3326    }
3327
3328    #[test]
3329    fn test_logger_contains_multiple_messages() {
3330        let logger = TestLogger::new();
3331
3332        logger.log_message(LogLevel::Info, "step 1 complete", 1);
3333        logger.log_message(LogLevel::Info, "step 2 complete", 2);
3334        logger.log_message(LogLevel::Info, "step 3 complete", 3);
3335
3336        // All messages should be findable
3337        assert!(logger.contains_message("step 1"));
3338        assert!(logger.contains_message("step 2"));
3339        assert!(logger.contains_message("step 3"));
3340        assert!(logger.contains_message("complete"));
3341        // Non-existent message
3342        assert!(!logger.contains_message("step 4"));
3343    }
3344
3345    #[test]
3346    fn test_log_capture_captures_logs_in_closure() {
3347        let capture = TestLogger::capture(|logger| {
3348            logger.log_message(LogLevel::Info, "inside capture", 1);
3349            logger.log_message(LogLevel::Warn, "warning inside", 2);
3350            42
3351        });
3352
3353        assert!(capture.passed());
3354        assert!(!capture.failed());
3355        assert_eq!(capture.result, Some(42));
3356        assert_eq!(capture.logs.len(), 2);
3357        assert!(capture.contains_message("inside capture"));
3358        assert!(capture.contains_message("warning inside"));
3359    }
3360
3361    #[test]
3362    fn test_log_capture_count_by_level() {
3363        let capture = TestLogger::capture(|logger| {
3364            logger.log_message(LogLevel::Info, "info 1", 1);
3365            logger.log_message(LogLevel::Info, "info 2", 2);
3366            logger.log_message(LogLevel::Info, "info 3", 3);
3367            logger.log_message(LogLevel::Error, "error 1", 4);
3368        });
3369
3370        assert_eq!(capture.count_by_level(LogLevel::Info), 3);
3371        assert_eq!(capture.count_by_level(LogLevel::Error), 1);
3372        assert_eq!(capture.count_by_level(LogLevel::Warn), 0);
3373    }
3374
3375    #[test]
3376    fn test_log_capture_phased_all_phases() {
3377        let capture = TestLogger::capture_phased(
3378            |logger| {
3379                logger.log_message(LogLevel::Info, "setup phase", 1);
3380            },
3381            |logger| {
3382                logger.log_message(LogLevel::Info, "execute phase", 2);
3383                "result"
3384            },
3385            |logger| {
3386                logger.log_message(LogLevel::Info, "teardown phase", 3);
3387            },
3388        );
3389
3390        assert!(capture.passed());
3391        assert_eq!(capture.result, Some("result"));
3392        assert_eq!(capture.logs.len(), 3);
3393        assert!(capture.contains_message("setup phase"));
3394        assert!(capture.contains_message("execute phase"));
3395        assert!(capture.contains_message("teardown phase"));
3396    }
3397
3398    #[test]
3399    fn test_log_capture_timings_recorded() {
3400        let capture = TestLogger::capture(|_logger| {
3401            // Small computation to ensure measurable time
3402            let mut sum = 0;
3403            for i in 0..1000 {
3404                sum += i;
3405            }
3406            sum
3407        });
3408
3409        assert!(capture.passed());
3410        // Timings should be recorded (may be very small but non-negative)
3411        let timings = &capture.timings;
3412        assert!(timings.total() >= std::time::Duration::ZERO);
3413    }
3414
3415    #[test]
3416    fn test_log_capture_failure_context() {
3417        let capture = TestLogger::capture(|logger| {
3418            logger.log_message(LogLevel::Info, "step 1", 1);
3419            logger.log_message(LogLevel::Info, "step 2", 2);
3420            logger.log_message(LogLevel::Error, "something went wrong", 3);
3421            logger.log_message(LogLevel::Info, "step 3", 4);
3422        });
3423
3424        let context = capture.failure_context(3);
3425        // Should contain the last 3 logs
3426        assert!(context.contains("something went wrong") || context.contains("step 3"));
3427    }
3428
3429    #[test]
3430    fn test_captured_log_format() {
3431        let log = CapturedLog::new(LogLevel::Warn, "test warning message", 12345);
3432
3433        let formatted = log.format();
3434        // Format is "[W] req=12345 test warning message" for Warn level
3435        assert!(formatted.contains("[W]"));
3436        assert!(formatted.contains("test warning message"));
3437        assert!(formatted.contains("12345"));
3438    }
3439
3440    #[test]
3441    fn test_captured_log_contains() {
3442        let log = CapturedLog::new(LogLevel::Info, "user login successful for alice", 1);
3443
3444        assert!(log.contains("login"));
3445        assert!(log.contains("alice"));
3446        assert!(log.contains("successful"));
3447        assert!(!log.contains("bob"));
3448        assert!(!log.contains("failed"));
3449    }
3450
3451    #[test]
3452    fn test_captured_log_fields() {
3453        let log = CapturedLog::new(LogLevel::Error, "database connection failed", 999);
3454
3455        assert_eq!(log.level, LogLevel::Error);
3456        assert_eq!(log.message, "database connection failed");
3457        assert_eq!(log.request_id, 999);
3458    }
3459
3460    #[test]
3461    fn test_multiple_loggers_isolated() {
3462        let logger1 = TestLogger::new();
3463        let logger2 = TestLogger::new();
3464
3465        logger1.log_message(LogLevel::Info, "from logger 1", 1);
3466        logger2.log_message(LogLevel::Info, "from logger 2", 2);
3467
3468        assert_eq!(logger1.logs().len(), 1);
3469        assert_eq!(logger2.logs().len(), 1);
3470        assert!(logger1.contains_message("logger 1"));
3471        assert!(!logger1.contains_message("logger 2"));
3472        assert!(logger2.contains_message("logger 2"));
3473        assert!(!logger2.contains_message("logger 1"));
3474    }
3475
3476    #[test]
3477    fn test_logger_log_entry_integration() {
3478        let logger = TestLogger::new();
3479
3480        let entry = LogEntry {
3481            level: LogLevel::Warn,
3482            message: "warning from entry".to_string(),
3483            request_id: 42,
3484            region_id: "region-1".to_string(),
3485            task_id: "task-1".to_string(),
3486            target: None,
3487            fields: Vec::new(),
3488            timestamp_ns: 0,
3489        };
3490
3491        logger.log_entry(&entry);
3492
3493        assert_eq!(logger.logs().len(), 1);
3494        let captured = &logger.logs()[0];
3495        assert_eq!(captured.level, LogLevel::Warn);
3496        assert!(captured.contains("warning from entry"));
3497        assert_eq!(captured.request_id, 42);
3498    }
3499
3500    #[test]
3501    fn test_log_capture_unwrap_on_success() {
3502        let capture = TestLogger::capture(|_| 123);
3503        let value = capture.unwrap();
3504        assert_eq!(value, 123);
3505    }
3506
3507    #[test]
3508    fn test_log_capture_unwrap_or_on_success() {
3509        let capture = TestLogger::capture(|_| 456);
3510        let value = capture.unwrap_or(0);
3511        assert_eq!(value, 456);
3512    }
3513}
3514
3515// =============================================================================
3516// MockServer for Integration Testing
3517// =============================================================================
3518
3519use std::io::{Read as _, Write as _};
3520use std::net::{Shutdown, SocketAddr, TcpListener as StdTcpListener, TcpStream as StdTcpStream};
3521use std::sync::atomic::AtomicBool;
3522use std::thread;
3523use std::time::Duration;
3524
3525/// Sends a clean FIN on the write side after the HTTP response has been
3526/// flushed. macOS will RST the socket if both ends drop without an explicit
3527/// shutdown while data is still buffered in the receive queue, which surfaces
3528/// to clients as `ECONNRESET` on the next `read`. Calling
3529/// `shutdown(Shutdown::Write)` here makes the close graceful on every
3530/// supported platform.
3531fn graceful_close(stream: &StdTcpStream) {
3532    let _ = stream.shutdown(Shutdown::Write);
3533}
3534
3535/// A recorded request from the mock server.
3536///
3537/// Contains all information about a request that was made to the mock server,
3538/// useful for asserting that expected requests were made.
3539#[derive(Debug, Clone)]
3540pub struct RecordedRequest {
3541    /// The HTTP method (GET, POST, etc.)
3542    pub method: String,
3543    /// The request path (e.g., "/api/users")
3544    pub path: String,
3545    /// Query string if present (without the leading '?')
3546    pub query: Option<String>,
3547    /// Request headers as name-value pairs
3548    pub headers: Vec<(String, String)>,
3549    /// Request body as bytes
3550    pub body: Vec<u8>,
3551    /// Timestamp when the request was received
3552    pub timestamp: std::time::Instant,
3553}
3554
3555impl RecordedRequest {
3556    /// Returns the request body as a UTF-8 string.
3557    ///
3558    /// # Panics
3559    ///
3560    /// Panics if the body is not valid UTF-8.
3561    #[must_use]
3562    pub fn body_text(&self) -> &str {
3563        std::str::from_utf8(&self.body).expect("body is not valid UTF-8")
3564    }
3565
3566    /// Returns a header value by name (case-insensitive).
3567    #[must_use]
3568    pub fn header(&self, name: &str) -> Option<&str> {
3569        let name_lower = name.to_ascii_lowercase();
3570        self.headers
3571            .iter()
3572            .find(|(n, _)| n.to_ascii_lowercase() == name_lower)
3573            .map(|(_, v)| v.as_str())
3574    }
3575
3576    /// Returns the full URL including query string.
3577    #[must_use]
3578    pub fn url(&self) -> String {
3579        match &self.query {
3580            Some(q) => format!("{}?{}", self.path, q),
3581            None => self.path.clone(),
3582        }
3583    }
3584}
3585
3586/// Configuration for a canned response.
3587#[derive(Debug, Clone)]
3588pub struct MockResponse {
3589    /// HTTP status code
3590    pub status: u16,
3591    /// Response headers
3592    pub headers: Vec<(String, String)>,
3593    /// Response body
3594    pub body: Vec<u8>,
3595    /// Optional delay before sending response
3596    pub delay: Option<Duration>,
3597}
3598
3599impl Default for MockResponse {
3600    fn default() -> Self {
3601        Self {
3602            status: 200,
3603            headers: vec![("content-type".to_string(), "text/plain".to_string())],
3604            body: b"OK".to_vec(),
3605            delay: None,
3606        }
3607    }
3608}
3609
3610impl MockResponse {
3611    /// Creates a new mock response with 200 OK status.
3612    #[must_use]
3613    pub fn ok() -> Self {
3614        Self::default()
3615    }
3616
3617    /// Creates a mock response with the given status code.
3618    #[must_use]
3619    pub fn with_status(status: u16) -> Self {
3620        Self {
3621            status,
3622            ..Default::default()
3623        }
3624    }
3625
3626    /// Sets the response status code.
3627    #[must_use]
3628    pub fn status(mut self, status: u16) -> Self {
3629        self.status = status;
3630        self
3631    }
3632
3633    /// Adds a header to the response.
3634    #[must_use]
3635    pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
3636        self.headers.push((name.into(), value.into()));
3637        self
3638    }
3639
3640    /// Sets the response body.
3641    #[must_use]
3642    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
3643        self.body = body.into();
3644        self
3645    }
3646
3647    /// Sets the response body as a string.
3648    #[must_use]
3649    pub fn body_str(self, body: &str) -> Self {
3650        self.body(body.as_bytes().to_vec())
3651    }
3652
3653    /// Sets the response body as JSON.
3654    #[must_use]
3655    pub fn json<T: serde::Serialize>(mut self, value: &T) -> Self {
3656        self.body = serde_json::to_vec(value).expect("JSON serialization failed");
3657        self.headers
3658            .push(("content-type".to_string(), "application/json".to_string()));
3659        self
3660    }
3661
3662    /// Sets a delay before sending the response.
3663    #[must_use]
3664    pub fn delay(mut self, duration: Duration) -> Self {
3665        self.delay = Some(duration);
3666        self
3667    }
3668
3669    /// Formats the response as an HTTP response string.
3670    fn to_http_response(&self) -> Vec<u8> {
3671        let status_text = match self.status {
3672            200 => "OK",
3673            201 => "Created",
3674            204 => "No Content",
3675            400 => "Bad Request",
3676            401 => "Unauthorized",
3677            403 => "Forbidden",
3678            404 => "Not Found",
3679            500 => "Internal Server Error",
3680            502 => "Bad Gateway",
3681            503 => "Service Unavailable",
3682            504 => "Gateway Timeout",
3683            _ => "Unknown",
3684        };
3685
3686        let mut response = format!("HTTP/1.1 {} {}\r\n", self.status, status_text);
3687
3688        // Add content-length header
3689        response.push_str(&format!("content-length: {}\r\n", self.body.len()));
3690
3691        // Add other headers
3692        for (name, value) in &self.headers {
3693            response.push_str(&format!("{}: {}\r\n", name, value));
3694        }
3695
3696        response.push_str("\r\n");
3697
3698        let mut bytes = response.into_bytes();
3699        bytes.extend_from_slice(&self.body);
3700        bytes
3701    }
3702}
3703
3704/// A mock HTTP server for integration testing.
3705///
3706/// `MockServer` spawns an actual TCP server on a random port, allowing you to
3707/// test HTTP client code against a real server. It records all incoming requests
3708/// and allows you to configure canned responses.
3709///
3710/// # Features
3711///
3712/// - **Real TCP server**: Listens on an actual port for real HTTP connections
3713/// - **Request recording**: Records all requests for later assertions
3714/// - **Canned responses**: Configure responses for specific paths
3715/// - **Clean shutdown**: Server shuts down when dropped
3716///
3717/// # Example
3718///
3719/// ```ignore
3720/// use fastapi_core::testing::{MockServer, MockResponse};
3721///
3722/// // Start a mock server
3723/// let server = MockServer::start();
3724///
3725/// // Configure a response
3726/// server.mock_response("/api/users", MockResponse::ok().json(&vec!["Alice", "Bob"]));
3727///
3728/// // Make requests with your HTTP client
3729/// let url = format!("http://{}/api/users", server.addr());
3730/// // ... make request ...
3731///
3732/// // Assert requests were made
3733/// let requests = server.requests();
3734/// assert_eq!(requests.len(), 1);
3735/// assert_eq!(requests[0].path, "/api/users");
3736/// ```
3737pub struct MockServer {
3738    addr: SocketAddr,
3739    requests: Arc<Mutex<Vec<RecordedRequest>>>,
3740    responses: Arc<Mutex<HashMap<String, MockResponse>>>,
3741    default_response: Arc<Mutex<MockResponse>>,
3742    shutdown: Arc<AtomicBool>,
3743    handle: Option<thread::JoinHandle<()>>,
3744}
3745
3746impl MockServer {
3747    /// Starts a new mock server on a random available port.
3748    ///
3749    /// The server begins listening immediately and runs in a background thread.
3750    ///
3751    /// # Example
3752    ///
3753    /// ```ignore
3754    /// let server = MockServer::start();
3755    /// println!("Server listening on {}", server.addr());
3756    /// ```
3757    #[must_use]
3758    pub fn start() -> Self {
3759        Self::start_with_options(MockServerOptions::default())
3760    }
3761
3762    /// Starts a mock server with custom options.
3763    #[must_use]
3764    pub fn start_with_options(options: MockServerOptions) -> Self {
3765        // Bind to a random port
3766        let listener =
3767            StdTcpListener::bind("127.0.0.1:0").expect("Failed to bind mock server to port");
3768        let addr = listener.local_addr().expect("Failed to get local address");
3769
3770        // Set non-blocking for clean shutdown
3771        listener
3772            .set_nonblocking(true)
3773            .expect("Failed to set non-blocking");
3774
3775        let requests = Arc::new(Mutex::new(Vec::new()));
3776        let responses = Arc::new(Mutex::new(HashMap::new()));
3777        let default_response = Arc::new(Mutex::new(options.default_response));
3778        let shutdown = Arc::new(AtomicBool::new(false));
3779
3780        let requests_clone = Arc::clone(&requests);
3781        let responses_clone = Arc::clone(&responses);
3782        let default_response_clone = Arc::clone(&default_response);
3783        let shutdown_clone = Arc::clone(&shutdown);
3784        let read_timeout = options.read_timeout;
3785
3786        let handle = thread::spawn(move || {
3787            Self::server_loop(
3788                listener,
3789                requests_clone,
3790                responses_clone,
3791                default_response_clone,
3792                shutdown_clone,
3793                read_timeout,
3794            );
3795        });
3796
3797        Self {
3798            addr,
3799            requests,
3800            responses,
3801            default_response,
3802            shutdown,
3803            handle: Some(handle),
3804        }
3805    }
3806
3807    /// The main server loop.
3808    fn server_loop(
3809        listener: StdTcpListener,
3810        requests: Arc<Mutex<Vec<RecordedRequest>>>,
3811        responses: Arc<Mutex<HashMap<String, MockResponse>>>,
3812        default_response: Arc<Mutex<MockResponse>>,
3813        shutdown: Arc<AtomicBool>,
3814        read_timeout: Duration,
3815    ) {
3816        loop {
3817            if shutdown.load(std::sync::atomic::Ordering::Acquire) {
3818                break;
3819            }
3820
3821            match listener.accept() {
3822                Ok((stream, _peer)) => {
3823                    // macOS accepts inherit the listener's non-blocking flag — force
3824                    // the accepted stream back to blocking mode so the synchronous
3825                    // request/response flow in `handle_connection` doesn't see
3826                    // `WouldBlock` on the very first read.
3827                    let _ = stream.set_nonblocking(false);
3828
3829                    // Handle the connection
3830                    let requests = Arc::clone(&requests);
3831                    let responses = Arc::clone(&responses);
3832                    let default_response = Arc::clone(&default_response);
3833
3834                    // Handle connection in the same thread (simple mock server)
3835                    Self::handle_connection(
3836                        stream,
3837                        requests,
3838                        responses,
3839                        default_response,
3840                        read_timeout,
3841                    );
3842                }
3843                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
3844                    // No connection available, sleep briefly and try again
3845                    thread::sleep(Duration::from_millis(10));
3846                }
3847                Err(e) => {
3848                    eprintln!("MockServer accept error: {}", e);
3849                    break;
3850                }
3851            }
3852        }
3853    }
3854
3855    /// Handles a single connection.
3856    fn handle_connection(
3857        mut stream: StdTcpStream,
3858        requests: Arc<Mutex<Vec<RecordedRequest>>>,
3859        responses: Arc<Mutex<HashMap<String, MockResponse>>>,
3860        default_response: Arc<Mutex<MockResponse>>,
3861        read_timeout: Duration,
3862    ) {
3863        // Set read timeout
3864        let _ = stream.set_read_timeout(Some(read_timeout));
3865
3866        // Read the request
3867        let mut buffer = vec![0u8; 8192];
3868        let Ok(bytes_read) = stream.read(&mut buffer) else {
3869            return;
3870        };
3871
3872        if bytes_read == 0 {
3873            return;
3874        }
3875
3876        buffer.truncate(bytes_read);
3877
3878        // Parse the request
3879        let Some(recorded) = Self::parse_request(&buffer) else {
3880            return;
3881        };
3882
3883        // Record the request
3884        {
3885            let mut reqs = requests.lock();
3886            reqs.push(recorded.clone());
3887        }
3888
3889        // Find matching response
3890        let response = {
3891            let resps = responses.lock();
3892            match resps.get(&recorded.path) {
3893                Some(r) => r.clone(),
3894                None => {
3895                    // Check for pattern matches
3896                    let mut matched = None;
3897                    for (pattern, resp) in resps.iter() {
3898                        if pattern.ends_with('*') {
3899                            let prefix = &pattern[..pattern.len() - 1];
3900                            if recorded.path.starts_with(prefix) {
3901                                matched = Some(resp.clone());
3902                                break;
3903                            }
3904                        }
3905                    }
3906                    matched.unwrap_or_else(|| default_response.lock().clone())
3907                }
3908            }
3909        };
3910
3911        // Apply delay if configured
3912        if let Some(delay) = response.delay {
3913            thread::sleep(delay);
3914        }
3915
3916        // Send response
3917        let response_bytes = response.to_http_response();
3918        let _ = stream.write_all(&response_bytes);
3919        let _ = stream.flush();
3920        graceful_close(&stream);
3921    }
3922
3923    /// Parses an HTTP request from raw bytes.
3924    fn parse_request(data: &[u8]) -> Option<RecordedRequest> {
3925        let text = std::str::from_utf8(data).ok()?;
3926        let mut lines = text.lines();
3927
3928        // Parse request line
3929        let request_line = lines.next()?;
3930        let parts: Vec<&str> = request_line.split_whitespace().collect();
3931        if parts.len() < 2 {
3932            return None;
3933        }
3934
3935        let method = parts[0].to_string();
3936        let full_path = parts[1];
3937
3938        // Split path and query
3939        let (path, query) = if let Some(idx) = full_path.find('?') {
3940            (
3941                full_path[..idx].to_string(),
3942                Some(full_path[idx + 1..].to_string()),
3943            )
3944        } else {
3945            (full_path.to_string(), None)
3946        };
3947
3948        // Parse headers
3949        let mut headers = Vec::new();
3950        let mut content_length = 0usize;
3951        for line in lines.by_ref() {
3952            if line.is_empty() {
3953                break;
3954            }
3955            if let Some((name, value)) = line.split_once(':') {
3956                let name = name.trim().to_string();
3957                let value = value.trim().to_string();
3958                if name.eq_ignore_ascii_case("content-length") {
3959                    content_length = value.parse().unwrap_or(0);
3960                }
3961                headers.push((name, value));
3962            }
3963        }
3964
3965        // Parse body
3966        let body = if content_length > 0 {
3967            // Find the body start in the original data
3968            if let Some(body_start) = text.find("\r\n\r\n") {
3969                let body_start = body_start + 4;
3970                if body_start < data.len() {
3971                    data[body_start..].to_vec()
3972                } else {
3973                    Vec::new()
3974                }
3975            } else if let Some(body_start) = text.find("\n\n") {
3976                let body_start = body_start + 2;
3977                if body_start < data.len() {
3978                    data[body_start..].to_vec()
3979                } else {
3980                    Vec::new()
3981                }
3982            } else {
3983                Vec::new()
3984            }
3985        } else {
3986            Vec::new()
3987        };
3988
3989        Some(RecordedRequest {
3990            method,
3991            path,
3992            query,
3993            headers,
3994            body,
3995            timestamp: std::time::Instant::now(),
3996        })
3997    }
3998
3999    /// Returns the socket address the server is listening on.
4000    #[must_use]
4001    pub fn addr(&self) -> SocketAddr {
4002        self.addr
4003    }
4004
4005    /// Returns the base URL for the server (e.g., "http://127.0.0.1:12345").
4006    #[must_use]
4007    pub fn url(&self) -> String {
4008        format!("http://{}", self.addr)
4009    }
4010
4011    /// Returns a URL for the given path.
4012    #[must_use]
4013    pub fn url_for(&self, path: &str) -> String {
4014        let path = if path.starts_with('/') {
4015            path
4016        } else {
4017            &format!("/{}", path)
4018        };
4019        format!("http://{}{}", self.addr, path)
4020    }
4021
4022    /// Configures a canned response for a specific path.
4023    ///
4024    /// Use `*` at the end of the path for prefix matching.
4025    ///
4026    /// # Example
4027    ///
4028    /// ```ignore
4029    /// server.mock_response("/api/users", MockResponse::ok().json(&users));
4030    /// server.mock_response("/api/*", MockResponse::with_status(404));
4031    /// ```
4032    pub fn mock_response(&self, path: impl Into<String>, response: MockResponse) {
4033        let mut responses = self.responses.lock();
4034        responses.insert(path.into(), response);
4035    }
4036
4037    /// Sets the default response for unmatched paths.
4038    pub fn set_default_response(&self, response: MockResponse) {
4039        let mut default = self.default_response.lock();
4040        *default = response;
4041    }
4042
4043    /// Returns all recorded requests.
4044    #[must_use]
4045    pub fn requests(&self) -> Vec<RecordedRequest> {
4046        let requests = self.requests.lock();
4047        requests.clone()
4048    }
4049
4050    /// Returns the number of recorded requests.
4051    #[must_use]
4052    pub fn request_count(&self) -> usize {
4053        let requests = self.requests.lock();
4054        requests.len()
4055    }
4056
4057    /// Returns requests matching the given path.
4058    #[must_use]
4059    pub fn requests_for(&self, path: &str) -> Vec<RecordedRequest> {
4060        let requests = self.requests.lock();
4061        requests
4062            .iter()
4063            .filter(|r| r.path == path)
4064            .cloned()
4065            .collect()
4066    }
4067
4068    /// Returns the last recorded request.
4069    #[must_use]
4070    pub fn last_request(&self) -> Option<RecordedRequest> {
4071        let requests = self.requests.lock();
4072        requests.last().cloned()
4073    }
4074
4075    /// Clears all recorded requests.
4076    pub fn clear_requests(&self) {
4077        let mut requests = self.requests.lock();
4078        requests.clear();
4079    }
4080
4081    /// Clears all configured responses.
4082    pub fn clear_responses(&self) {
4083        let mut responses = self.responses.lock();
4084        responses.clear();
4085    }
4086
4087    /// Resets the server (clears requests and responses).
4088    pub fn reset(&self) {
4089        self.clear_requests();
4090        self.clear_responses();
4091    }
4092
4093    /// Waits for a specific number of requests, with timeout.
4094    ///
4095    /// Returns `true` if the expected number of requests were received,
4096    /// `false` if the timeout was reached.
4097    pub fn wait_for_requests(&self, count: usize, timeout: Duration) -> bool {
4098        let start = std::time::Instant::now();
4099        loop {
4100            if self.request_count() >= count {
4101                return true;
4102            }
4103            if start.elapsed() >= timeout {
4104                return false;
4105            }
4106            thread::sleep(Duration::from_millis(10));
4107        }
4108    }
4109
4110    /// Asserts that a request was made to the given path.
4111    ///
4112    /// # Panics
4113    ///
4114    /// Panics if no request was made to the path.
4115    pub fn assert_received(&self, path: &str) {
4116        let requests = self.requests_for(path);
4117        assert!(
4118            !requests.is_empty(),
4119            "Expected request to path '{}', but none was received. Received paths: {:?}",
4120            path,
4121            self.requests().iter().map(|r| &r.path).collect::<Vec<_>>()
4122        );
4123    }
4124
4125    /// Asserts that no request was made to the given path.
4126    ///
4127    /// # Panics
4128    ///
4129    /// Panics if a request was made to the path.
4130    pub fn assert_not_received(&self, path: &str) {
4131        let requests = self.requests_for(path);
4132        assert!(
4133            requests.is_empty(),
4134            "Expected no request to path '{}', but {} were received",
4135            path,
4136            requests.len()
4137        );
4138    }
4139
4140    /// Asserts the total number of requests received.
4141    ///
4142    /// # Panics
4143    ///
4144    /// Panics if the count doesn't match.
4145    pub fn assert_request_count(&self, expected: usize) {
4146        let actual = self.request_count();
4147        assert_eq!(
4148            actual, expected,
4149            "Expected {} requests, but received {}",
4150            expected, actual
4151        );
4152    }
4153}
4154
4155impl Drop for MockServer {
4156    fn drop(&mut self) {
4157        // Signal shutdown
4158        self.shutdown
4159            .store(true, std::sync::atomic::Ordering::Release);
4160
4161        // Wait for the server thread to finish
4162        if let Some(handle) = self.handle.take() {
4163            let _ = handle.join();
4164        }
4165    }
4166}
4167
4168/// Options for configuring a MockServer.
4169#[derive(Debug, Clone)]
4170pub struct MockServerOptions {
4171    /// Default response for unmatched paths.
4172    pub default_response: MockResponse,
4173    /// Read timeout for connections.
4174    pub read_timeout: Duration,
4175}
4176
4177impl Default for MockServerOptions {
4178    fn default() -> Self {
4179        Self {
4180            default_response: MockResponse::with_status(404).body_str("Not Found"),
4181            read_timeout: Duration::from_secs(5),
4182        }
4183    }
4184}
4185
4186impl MockServerOptions {
4187    /// Creates new options with default values.
4188    #[must_use]
4189    pub fn new() -> Self {
4190        Self::default()
4191    }
4192
4193    /// Sets the default response.
4194    #[must_use]
4195    pub fn default_response(mut self, response: MockResponse) -> Self {
4196        self.default_response = response;
4197        self
4198    }
4199
4200    /// Sets the read timeout.
4201    #[must_use]
4202    pub fn read_timeout(mut self, timeout: Duration) -> Self {
4203        self.read_timeout = timeout;
4204        self
4205    }
4206}
4207
4208// =============================================================================
4209// Real HTTP Test Server
4210// =============================================================================
4211
4212/// A log entry recorded by [`TestServer`] for each processed request.
4213///
4214/// This provides structured logging of all HTTP traffic flowing through
4215/// the test server, useful for debugging test failures.
4216#[derive(Debug, Clone)]
4217pub struct TestServerLogEntry {
4218    /// HTTP method (e.g., "GET", "POST").
4219    pub method: String,
4220    /// Request path (e.g., "/api/users").
4221    pub path: String,
4222    /// Response status code.
4223    pub status: u16,
4224    /// Time taken to process the request through the App pipeline.
4225    pub duration: Duration,
4226    /// When this request was received.
4227    pub timestamp: std::time::Instant,
4228}
4229
4230/// Configuration for [`TestServer`].
4231#[derive(Debug, Clone)]
4232pub struct TestServerConfig {
4233    /// TCP read timeout for connections (default: 5 seconds).
4234    pub read_timeout: Duration,
4235    /// Whether to log each request/response (default: true).
4236    pub log_requests: bool,
4237}
4238
4239impl Default for TestServerConfig {
4240    fn default() -> Self {
4241        Self {
4242            read_timeout: Duration::from_secs(5),
4243            log_requests: true,
4244        }
4245    }
4246}
4247
4248impl TestServerConfig {
4249    /// Creates a new configuration with default values.
4250    #[must_use]
4251    pub fn new() -> Self {
4252        Self::default()
4253    }
4254
4255    /// Sets the read timeout for TCP connections.
4256    #[must_use]
4257    pub fn read_timeout(mut self, timeout: Duration) -> Self {
4258        self.read_timeout = timeout;
4259        self
4260    }
4261
4262    /// Sets whether to log requests.
4263    #[must_use]
4264    pub fn log_requests(mut self, log: bool) -> Self {
4265        self.log_requests = log;
4266        self
4267    }
4268}
4269
4270/// A real HTTP test server that routes requests through the full App pipeline.
4271///
4272/// Unlike [`TestClient`] which operates in-process without network I/O,
4273/// `TestServer` creates actual TCP connections and processes requests through
4274/// the complete HTTP parsing -> App.handle() -> response serialization pipeline.
4275///
4276/// This enables true end-to-end testing including:
4277/// - HTTP request parsing from raw bytes
4278/// - Full middleware stack execution
4279/// - Route matching and handler dispatch
4280/// - Response serialization to HTTP/1.1
4281/// - Cookie handling over the wire
4282/// - Keep-alive and connection management
4283///
4284/// # Architecture
4285///
4286/// ```text
4287/// Test Code                        TestServer (background thread)
4288///     |                                 |
4289///     |-- TCP connect ----------------> |
4290///     |-- Send HTTP request ----------> |
4291///     |                                 |-- Parse HTTP request
4292///     |                                 |-- Create RequestContext
4293///     |                                 |-- App.handle(ctx, req)
4294///     |                                 |-- Serialize Response
4295///     |<-- Receive HTTP response ------ |
4296///     |                                 |-- Log entry recorded
4297/// ```
4298///
4299/// # Example
4300///
4301/// ```ignore
4302/// use fastapi_core::testing::TestServer;
4303/// use fastapi_core::app::App;
4304/// use std::io::{Read, Write};
4305/// use std::net::TcpStream;
4306///
4307/// let app = App::builder()
4308///     .get("/health", |_, _| async { Response::ok().body_text("OK") })
4309///     .build();
4310///
4311/// let server = TestServer::start(app);
4312/// println!("Server running on {}", server.url());
4313///
4314/// // Connect with any HTTP client
4315/// let mut stream = TcpStream::connect(server.addr()).unwrap();
4316/// stream.write_all(b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n").unwrap();
4317///
4318/// let mut buf = vec![0u8; 4096];
4319/// let n = stream.read(&mut buf).unwrap();
4320/// let response = String::from_utf8_lossy(&buf[..n]);
4321/// assert!(response.contains("200 OK"));
4322///
4323/// // Check server logs
4324/// let logs = server.log_entries();
4325/// assert_eq!(logs.len(), 1);
4326/// assert_eq!(logs[0].path, "/health");
4327/// assert_eq!(logs[0].status, 200);
4328/// ```
4329pub struct TestServer {
4330    addr: SocketAddr,
4331    shutdown: Arc<AtomicBool>,
4332    handle: Option<thread::JoinHandle<()>>,
4333    log_entries: Arc<Mutex<Vec<TestServerLogEntry>>>,
4334    shutdown_controller: crate::shutdown::ShutdownController,
4335}
4336
4337impl TestServer {
4338    /// Starts a new test server with the given App on a random available port.
4339    ///
4340    /// The server begins listening immediately and runs in a background thread.
4341    /// It will process requests through the full App pipeline including all
4342    /// middleware, routing, and error handling.
4343    ///
4344    /// # Panics
4345    ///
4346    /// Panics if binding to a local port fails.
4347    #[must_use]
4348    pub fn start(app: crate::app::App) -> Self {
4349        Self::start_with_config(app, TestServerConfig::default())
4350    }
4351
4352    /// Starts a test server with custom configuration.
4353    #[must_use]
4354    pub fn start_with_config(app: crate::app::App, config: TestServerConfig) -> Self {
4355        let listener =
4356            StdTcpListener::bind("127.0.0.1:0").expect("Failed to bind test server to port");
4357        let addr = listener.local_addr().expect("Failed to get local address");
4358
4359        listener
4360            .set_nonblocking(true)
4361            .expect("Failed to set non-blocking");
4362
4363        let app = Arc::new(app);
4364        let shutdown = Arc::new(AtomicBool::new(false));
4365        let log_entries = Arc::new(Mutex::new(Vec::new()));
4366        let shutdown_controller = crate::shutdown::ShutdownController::new();
4367
4368        let shutdown_clone = Arc::clone(&shutdown);
4369        let log_entries_clone = Arc::clone(&log_entries);
4370        let app_clone = Arc::clone(&app);
4371        let controller_clone = shutdown_controller.clone();
4372
4373        let handle = thread::spawn(move || {
4374            Self::server_loop(
4375                listener,
4376                app_clone,
4377                shutdown_clone,
4378                log_entries_clone,
4379                config,
4380                controller_clone,
4381            );
4382        });
4383
4384        Self {
4385            addr,
4386            shutdown,
4387            handle: Some(handle),
4388            log_entries,
4389            shutdown_controller,
4390        }
4391    }
4392
4393    /// The main server loop — accepts connections and processes requests.
4394    fn server_loop(
4395        listener: StdTcpListener,
4396        app: Arc<crate::app::App>,
4397        shutdown: Arc<AtomicBool>,
4398        log_entries: Arc<Mutex<Vec<TestServerLogEntry>>>,
4399        config: TestServerConfig,
4400        controller: crate::shutdown::ShutdownController,
4401    ) {
4402        let request_counter = std::sync::atomic::AtomicU64::new(1);
4403
4404        loop {
4405            if shutdown.load(std::sync::atomic::Ordering::Acquire) {
4406                // Run shutdown hooks before exiting
4407                while let Some(hook) = controller.pop_hook() {
4408                    hook.run();
4409                }
4410                break;
4411            }
4412
4413            match listener.accept() {
4414                Ok((stream, _peer)) => {
4415                    // Track in-flight requests
4416                    let _guard = controller.track_request();
4417
4418                    // macOS accepts inherit the listener's non-blocking flag, so we must
4419                    // force the new stream back to blocking mode before doing the
4420                    // request-then-response synchronous I/O dance below; otherwise the
4421                    // first `read` returns `WouldBlock`, we silently drop the stream,
4422                    // and the client sees an empty response.
4423                    let _ = stream.set_nonblocking(false);
4424
4425                    // If shutting down, reject with 503
4426                    if controller.is_shutting_down() {
4427                        Self::send_503(stream);
4428                        continue;
4429                    }
4430
4431                    Self::handle_connection(stream, &app, &log_entries, &config, &request_counter);
4432                }
4433                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
4434                    thread::sleep(Duration::from_millis(5));
4435                }
4436                Err(_) => {
4437                    break;
4438                }
4439            }
4440        }
4441    }
4442
4443    /// Handles a single TCP connection, potentially with keep-alive.
4444    fn handle_connection(
4445        mut stream: StdTcpStream,
4446        app: &Arc<crate::app::App>,
4447        log_entries: &Arc<Mutex<Vec<TestServerLogEntry>>>,
4448        config: &TestServerConfig,
4449        request_counter: &std::sync::atomic::AtomicU64,
4450    ) {
4451        let _ = stream.set_read_timeout(Some(config.read_timeout));
4452
4453        // Read the request data
4454        let mut buffer = vec![0u8; 65536];
4455        let bytes_read = match stream.read(&mut buffer) {
4456            Ok(n) if n > 0 => n,
4457            _ => return,
4458        };
4459        buffer.truncate(bytes_read);
4460
4461        // Parse the raw HTTP request into method, path, headers, body
4462        let Some(parsed) = Self::parse_raw_request(&buffer) else {
4463            // Send 400 Bad Request for unparseable requests
4464            let bad_request = b"HTTP/1.1 400 Bad Request\r\ncontent-length: 11\r\n\r\nBad Request";
4465            let _ = stream.write_all(bad_request);
4466            let _ = stream.flush();
4467            graceful_close(&stream);
4468            return;
4469        };
4470
4471        let start_time = std::time::Instant::now();
4472
4473        // Build a proper Request object
4474        let method = match parsed.method.to_uppercase().as_str() {
4475            "GET" => Method::Get,
4476            "POST" => Method::Post,
4477            "PUT" => Method::Put,
4478            "DELETE" => Method::Delete,
4479            "PATCH" => Method::Patch,
4480            "HEAD" => Method::Head,
4481            "OPTIONS" => Method::Options,
4482            _ => Method::Get,
4483        };
4484
4485        let mut request = Request::new(method, &parsed.path);
4486
4487        // Set query string if present
4488        if let Some(ref query) = parsed.query {
4489            request.set_query(Some(query.clone()));
4490        }
4491
4492        // Copy headers
4493        for (name, value) in &parsed.headers {
4494            request
4495                .headers_mut()
4496                .insert(name.clone(), value.as_bytes().to_vec());
4497        }
4498
4499        // Set body
4500        if !parsed.body.is_empty() {
4501            request.set_body(Body::Bytes(parsed.body.clone()));
4502        }
4503
4504        // Create RequestContext with a Cx for testing
4505        let cx = Cx::for_testing();
4506        let request_id = request_counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
4507        let dependency_overrides = Handler::dependency_overrides(app.as_ref())
4508            .unwrap_or_else(|| Arc::new(crate::dependency::DependencyOverrides::new()));
4509        let ctx = RequestContext::with_overrides(cx, request_id, dependency_overrides);
4510
4511        // Execute the App handler synchronously
4512        let response = futures_executor::block_on(app.handle(&ctx, &mut request));
4513
4514        let duration = start_time.elapsed();
4515        let status_code = response.status().as_u16();
4516
4517        // Log the request if configured
4518        if config.log_requests {
4519            let entry = TestServerLogEntry {
4520                method: parsed.method.clone(),
4521                path: parsed.path.clone(),
4522                status: status_code,
4523                duration,
4524                timestamp: start_time,
4525            };
4526            log_entries.lock().push(entry);
4527        }
4528
4529        // Serialize the Response to HTTP/1.1 bytes and send
4530        let response_bytes = Self::serialize_response(response);
4531        let _ = stream.write_all(&response_bytes);
4532        let _ = stream.flush();
4533        graceful_close(&stream);
4534    }
4535
4536    /// Parses raw HTTP request bytes into structured components.
4537    fn parse_raw_request(data: &[u8]) -> Option<ParsedRequest> {
4538        let text = std::str::from_utf8(data).ok()?;
4539        let mut lines = text.lines();
4540
4541        // Parse request line: "GET /path HTTP/1.1"
4542        let request_line = lines.next()?;
4543        let parts: Vec<&str> = request_line.split_whitespace().collect();
4544        if parts.len() < 2 {
4545            return None;
4546        }
4547
4548        let method = parts[0].to_string();
4549        let full_path = parts[1];
4550
4551        // Split path and query string
4552        let (path, query) = if let Some(idx) = full_path.find('?') {
4553            (
4554                full_path[..idx].to_string(),
4555                Some(full_path[idx + 1..].to_string()),
4556            )
4557        } else {
4558            (full_path.to_string(), None)
4559        };
4560
4561        // Parse headers
4562        let mut headers = Vec::new();
4563        let mut content_length = 0usize;
4564        for line in lines.by_ref() {
4565            if line.is_empty() {
4566                break;
4567            }
4568            if let Some((name, value)) = line.split_once(':') {
4569                let name = name.trim().to_string();
4570                let value = value.trim().to_string();
4571                if name.eq_ignore_ascii_case("content-length") {
4572                    content_length = value.parse().unwrap_or(0);
4573                }
4574                headers.push((name, value));
4575            }
4576        }
4577
4578        // Parse body
4579        let body = if content_length > 0 {
4580            if let Some(body_start) = text.find("\r\n\r\n") {
4581                let body_start = body_start + 4;
4582                if body_start < data.len() {
4583                    data[body_start..].to_vec()
4584                } else {
4585                    Vec::new()
4586                }
4587            } else if let Some(body_start) = text.find("\n\n") {
4588                let body_start = body_start + 2;
4589                if body_start < data.len() {
4590                    data[body_start..].to_vec()
4591                } else {
4592                    Vec::new()
4593                }
4594            } else {
4595                Vec::new()
4596            }
4597        } else {
4598            Vec::new()
4599        };
4600
4601        Some(ParsedRequest {
4602            method,
4603            path,
4604            query,
4605            headers,
4606            body,
4607        })
4608    }
4609
4610    /// Serializes a Response to HTTP/1.1 wire format bytes.
4611    fn serialize_response(response: Response) -> Vec<u8> {
4612        let (status, headers, body) = response.into_parts();
4613
4614        let body_bytes = match body {
4615            ResponseBody::Empty => Vec::new(),
4616            ResponseBody::Bytes(b) => b,
4617            ResponseBody::Stream(_) => {
4618                // For streaming responses in test context, we can't easily
4619                // collect the stream synchronously. Return empty body.
4620                Vec::new()
4621            }
4622        };
4623
4624        let mut buf = Vec::with_capacity(512 + body_bytes.len());
4625
4626        // Status line
4627        buf.extend_from_slice(b"HTTP/1.1 ");
4628        buf.extend_from_slice(status.as_u16().to_string().as_bytes());
4629        buf.extend_from_slice(b" ");
4630        buf.extend_from_slice(status.canonical_reason().as_bytes());
4631        buf.extend_from_slice(b"\r\n");
4632
4633        // Headers (skip content-length and transfer-encoding; we'll add our own)
4634        for (name, value) in &headers {
4635            if name.eq_ignore_ascii_case("content-length")
4636                || name.eq_ignore_ascii_case("transfer-encoding")
4637            {
4638                continue;
4639            }
4640            buf.extend_from_slice(name.as_bytes());
4641            buf.extend_from_slice(b": ");
4642            buf.extend_from_slice(value);
4643            buf.extend_from_slice(b"\r\n");
4644        }
4645
4646        // Content-Length
4647        buf.extend_from_slice(b"content-length: ");
4648        buf.extend_from_slice(body_bytes.len().to_string().as_bytes());
4649        buf.extend_from_slice(b"\r\n");
4650
4651        // End of headers
4652        buf.extend_from_slice(b"\r\n");
4653
4654        // Body
4655        buf.extend_from_slice(&body_bytes);
4656
4657        buf
4658    }
4659
4660    /// Returns the socket address the server is listening on.
4661    #[must_use]
4662    pub fn addr(&self) -> SocketAddr {
4663        self.addr
4664    }
4665
4666    /// Returns the port the server is listening on.
4667    #[must_use]
4668    pub fn port(&self) -> u16 {
4669        self.addr.port()
4670    }
4671
4672    /// Returns the base URL (e.g., "http://127.0.0.1:12345").
4673    #[must_use]
4674    pub fn url(&self) -> String {
4675        format!("http://{}", self.addr)
4676    }
4677
4678    /// Returns a URL for the given path.
4679    #[must_use]
4680    pub fn url_for(&self, path: &str) -> String {
4681        let path = if path.starts_with('/') {
4682            path.to_string()
4683        } else {
4684            format!("/{path}")
4685        };
4686        format!("http://{}{}", self.addr, path)
4687    }
4688
4689    /// Returns a snapshot of all log entries recorded so far.
4690    #[must_use]
4691    pub fn log_entries(&self) -> Vec<TestServerLogEntry> {
4692        self.log_entries.lock().clone()
4693    }
4694
4695    /// Returns the number of requests processed.
4696    #[must_use]
4697    pub fn request_count(&self) -> usize {
4698        self.log_entries.lock().len()
4699    }
4700
4701    /// Clears all recorded log entries.
4702    pub fn clear_logs(&self) {
4703        self.log_entries.lock().clear();
4704    }
4705
4706    /// Sends a 503 Service Unavailable response during shutdown.
4707    fn send_503(mut stream: StdTcpStream) {
4708        let response =
4709            b"HTTP/1.1 503 Service Unavailable\r\ncontent-length: 19\r\n\r\nService Unavailable";
4710        let _ = stream.write_all(response);
4711        let _ = stream.flush();
4712        graceful_close(&stream);
4713    }
4714
4715    /// Returns a reference to the server's shutdown controller.
4716    ///
4717    /// Use this to coordinate graceful shutdown in tests, including:
4718    /// - Tracking in-flight requests via [`crate::ShutdownController::track_request`]
4719    /// - Registering shutdown hooks via [`crate::ShutdownController::register_hook`]
4720    /// - Checking shutdown phase via [`crate::ShutdownController::phase`]
4721    #[must_use]
4722    pub fn shutdown_controller(&self) -> &crate::shutdown::ShutdownController {
4723        &self.shutdown_controller
4724    }
4725
4726    /// Returns the number of currently in-flight requests.
4727    #[must_use]
4728    pub fn in_flight_count(&self) -> usize {
4729        self.shutdown_controller.in_flight_count()
4730    }
4731
4732    /// Signals the server to shut down gracefully.
4733    ///
4734    /// This triggers the shutdown controller (which will cause the server
4735    /// to reject new requests with 503) and stops the accept loop.
4736    /// This is also called automatically on drop.
4737    pub fn shutdown(&self) {
4738        self.shutdown_controller.shutdown();
4739        self.shutdown
4740            .store(true, std::sync::atomic::Ordering::Release);
4741    }
4742
4743    /// Returns true if the server has been signaled to shut down.
4744    #[must_use]
4745    pub fn is_shutdown(&self) -> bool {
4746        self.shutdown.load(std::sync::atomic::Ordering::Acquire)
4747    }
4748}
4749
4750impl Drop for TestServer {
4751    fn drop(&mut self) {
4752        self.shutdown
4753            .store(true, std::sync::atomic::Ordering::Release);
4754        if let Some(handle) = self.handle.take() {
4755            let _ = handle.join();
4756        }
4757    }
4758}
4759
4760/// Internal parsed request for TestServer (not the same as Request).
4761struct ParsedRequest {
4762    method: String,
4763    path: String,
4764    query: Option<String>,
4765    headers: Vec<(String, String)>,
4766    body: Vec<u8>,
4767}
4768
4769// =============================================================================
4770// E2E Testing Framework
4771// =============================================================================
4772
4773/// Result of executing an E2E step.
4774#[derive(Debug, Clone)]
4775pub enum E2EStepResult {
4776    /// Step passed successfully.
4777    Passed,
4778    /// Step failed with an error message.
4779    Failed(String),
4780    /// Step was skipped (e.g., due to prior failure).
4781    Skipped,
4782}
4783
4784impl E2EStepResult {
4785    /// Returns `true` if the step passed.
4786    #[must_use]
4787    pub fn is_passed(&self) -> bool {
4788        matches!(self, Self::Passed)
4789    }
4790
4791    /// Returns `true` if the step failed.
4792    #[must_use]
4793    pub fn is_failed(&self) -> bool {
4794        matches!(self, Self::Failed(_))
4795    }
4796}
4797
4798/// A captured HTTP request/response pair from an E2E step.
4799#[derive(Debug, Clone)]
4800pub struct E2ECapture {
4801    /// The request method.
4802    pub method: String,
4803    /// The request path.
4804    pub path: String,
4805    /// Request headers.
4806    pub request_headers: Vec<(String, String)>,
4807    /// Request body (if any).
4808    pub request_body: Option<String>,
4809    /// Response status code.
4810    pub response_status: u16,
4811    /// Response headers.
4812    pub response_headers: Vec<(String, String)>,
4813    /// Response body.
4814    pub response_body: String,
4815}
4816
4817/// A single step in an E2E test scenario.
4818#[derive(Debug, Clone)]
4819pub struct E2EStep {
4820    /// Step name/description.
4821    pub name: String,
4822    /// When the step started.
4823    pub started_at: std::time::Instant,
4824    /// Step duration.
4825    pub duration: std::time::Duration,
4826    /// Step result.
4827    pub result: E2EStepResult,
4828    /// Captured request/response (if applicable).
4829    pub capture: Option<E2ECapture>,
4830}
4831
4832impl E2EStep {
4833    /// Creates a new step record.
4834    fn new(name: impl Into<String>) -> Self {
4835        Self {
4836            name: name.into(),
4837            started_at: std::time::Instant::now(),
4838            duration: std::time::Duration::ZERO,
4839            result: E2EStepResult::Skipped,
4840            capture: None,
4841        }
4842    }
4843
4844    /// Marks the step as complete with a result.
4845    fn complete(&mut self, result: E2EStepResult) {
4846        self.duration = self.started_at.elapsed();
4847        self.result = result;
4848    }
4849}
4850
4851/// E2E test scenario builder and executor.
4852///
4853/// Provides structured E2E testing with step logging, timing, and detailed
4854/// failure reporting. Automatically captures request/response data on failures.
4855///
4856/// # Example
4857///
4858/// ```ignore
4859/// use fastapi_core::testing::{E2EScenario, TestClient};
4860///
4861/// let client = TestClient::new(app);
4862/// let mut scenario = E2EScenario::new("User Registration Flow", client);
4863///
4864/// scenario.step("Visit registration page", |client| {
4865///     let response = client.get("/register").send();
4866///     assert_eq!(response.status().as_u16(), 200);
4867/// });
4868///
4869/// scenario.step("Submit registration form", |client| {
4870///     let response = client
4871///         .post("/register")
4872///         .json(&serde_json::json!({"email": "test@example.com", "password": "secret123"}))
4873///         .send();
4874///     assert_eq!(response.status().as_u16(), 201);
4875/// });
4876///
4877/// // Generate report
4878/// let report = scenario.report();
4879/// println!("{}", report.to_text());
4880/// ```
4881pub struct E2EScenario<H> {
4882    /// Scenario name.
4883    name: String,
4884    /// Description of what this scenario tests.
4885    description: Option<String>,
4886    /// The test client.
4887    client: TestClient<H>,
4888    /// Recorded steps.
4889    steps: Vec<E2EStep>,
4890    /// Whether to stop on first failure.
4891    stop_on_failure: bool,
4892    /// Whether a failure has occurred.
4893    has_failure: bool,
4894    /// Captured output for logging.
4895    log_buffer: Vec<String>,
4896}
4897
4898impl<H: Handler + 'static> E2EScenario<H> {
4899    /// Creates a new E2E scenario.
4900    pub fn new(name: impl Into<String>, client: TestClient<H>) -> Self {
4901        let name = name.into();
4902        Self {
4903            name,
4904            description: None,
4905            client,
4906            steps: Vec::new(),
4907            stop_on_failure: true,
4908            has_failure: false,
4909            log_buffer: Vec::new(),
4910        }
4911    }
4912
4913    /// Sets the scenario description.
4914    #[must_use]
4915    pub fn description(mut self, desc: impl Into<String>) -> Self {
4916        self.description = Some(desc.into());
4917        self
4918    }
4919
4920    /// Configures whether to stop on first failure (default: true).
4921    #[must_use]
4922    pub fn stop_on_failure(mut self, stop: bool) -> Self {
4923        self.stop_on_failure = stop;
4924        self
4925    }
4926
4927    /// Returns a reference to the test client.
4928    pub fn client(&self) -> &TestClient<H> {
4929        &self.client
4930    }
4931
4932    /// Returns a mutable reference to the test client.
4933    pub fn client_mut(&mut self) -> &mut TestClient<H> {
4934        &mut self.client
4935    }
4936
4937    /// Logs a message to the scenario log.
4938    pub fn log(&mut self, message: impl Into<String>) {
4939        let msg = message.into();
4940        self.log_buffer.push(format!(
4941            "[{:?}] {}",
4942            std::time::Instant::now().elapsed(),
4943            msg
4944        ));
4945    }
4946
4947    /// Executes a step in the scenario.
4948    ///
4949    /// The step function receives a reference to the test client and should
4950    /// perform assertions. Panics are caught and recorded as failures.
4951    pub fn step<F>(&mut self, name: impl Into<String>, f: F)
4952    where
4953        F: FnOnce(&TestClient<H>) + std::panic::UnwindSafe,
4954    {
4955        let name = name.into();
4956        let mut step = E2EStep::new(&name);
4957
4958        // Skip if we've already failed and stop_on_failure is enabled
4959        if self.has_failure && self.stop_on_failure {
4960            step.complete(E2EStepResult::Skipped);
4961            self.log_buffer.push(format!("[SKIP] {}", name));
4962            self.steps.push(step);
4963            return;
4964        }
4965
4966        self.log_buffer.push(format!("[START] {}", name));
4967
4968        // Wrap client in AssertUnwindSafe for panic catching
4969        let client_ref = std::panic::AssertUnwindSafe(&self.client);
4970
4971        // Execute the step and catch any panics
4972        let result = std::panic::catch_unwind(|| {
4973            f(&client_ref);
4974        });
4975
4976        match result {
4977            Ok(()) => {
4978                step.complete(E2EStepResult::Passed);
4979                self.log_buffer
4980                    .push(format!("[PASS] {} ({:?})", name, step.duration));
4981            }
4982            Err(panic_info) => {
4983                let error_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
4984                    (*s).to_string()
4985                } else if let Some(s) = panic_info.downcast_ref::<String>() {
4986                    s.clone()
4987                } else {
4988                    "Unknown panic".to_string()
4989                };
4990
4991                step.complete(E2EStepResult::Failed(error_msg.clone()));
4992                self.has_failure = true;
4993                self.log_buffer
4994                    .push(format!("[FAIL] {} - {}", name, error_msg));
4995            }
4996        }
4997
4998        self.steps.push(step);
4999    }
5000
5001    /// Executes a step that returns a result (for more control over error handling).
5002    pub fn try_step<F, E>(&mut self, name: impl Into<String>, f: F) -> Result<(), E>
5003    where
5004        F: FnOnce(&TestClient<H>) -> Result<(), E>,
5005        E: std::fmt::Display,
5006    {
5007        let name = name.into();
5008        let mut step = E2EStep::new(&name);
5009
5010        if self.has_failure && self.stop_on_failure {
5011            step.complete(E2EStepResult::Skipped);
5012            self.steps.push(step);
5013            return Ok(());
5014        }
5015
5016        self.log_buffer.push(format!("[START] {}", name));
5017
5018        match f(&self.client) {
5019            Ok(()) => {
5020                step.complete(E2EStepResult::Passed);
5021                self.log_buffer
5022                    .push(format!("[PASS] {} ({:?})", name, step.duration));
5023                self.steps.push(step);
5024                Ok(())
5025            }
5026            Err(e) => {
5027                let error_msg = e.to_string();
5028                step.complete(E2EStepResult::Failed(error_msg.clone()));
5029                self.has_failure = true;
5030                self.log_buffer
5031                    .push(format!("[FAIL] {} - {}", name, error_msg));
5032                self.steps.push(step);
5033                Err(e)
5034            }
5035        }
5036    }
5037
5038    /// Returns whether the scenario passed (no failures).
5039    #[must_use]
5040    pub fn passed(&self) -> bool {
5041        !self.has_failure
5042    }
5043
5044    /// Returns the steps executed so far.
5045    #[must_use]
5046    pub fn steps(&self) -> &[E2EStep] {
5047        &self.steps
5048    }
5049
5050    /// Returns the log buffer.
5051    #[must_use]
5052    pub fn logs(&self) -> &[String] {
5053        &self.log_buffer
5054    }
5055
5056    /// Generates a test report.
5057    #[must_use]
5058    pub fn report(&self) -> E2EReport {
5059        let passed = self.steps.iter().filter(|s| s.result.is_passed()).count();
5060        let failed = self.steps.iter().filter(|s| s.result.is_failed()).count();
5061        let skipped = self
5062            .steps
5063            .iter()
5064            .filter(|s| matches!(s.result, E2EStepResult::Skipped))
5065            .count();
5066        let total_duration: std::time::Duration = self.steps.iter().map(|s| s.duration).sum();
5067
5068        E2EReport {
5069            scenario_name: self.name.clone(),
5070            description: self.description.clone(),
5071            passed,
5072            failed,
5073            skipped,
5074            total_duration,
5075            steps: self.steps.clone(),
5076            logs: self.log_buffer.clone(),
5077        }
5078    }
5079
5080    /// Asserts that the scenario passed, panicking with a detailed report if not.
5081    ///
5082    /// Call this at the end of your test to ensure all steps passed.
5083    pub fn assert_passed(&self) {
5084        if !self.passed() {
5085            let report = self.report();
5086            panic!(
5087                "E2E Scenario '{}' failed!\n\n{}",
5088                self.name,
5089                report.to_text()
5090            );
5091        }
5092    }
5093}
5094
5095/// E2E test report with multiple output formats.
5096#[derive(Debug, Clone)]
5097pub struct E2EReport {
5098    /// Scenario name.
5099    pub scenario_name: String,
5100    /// Scenario description.
5101    pub description: Option<String>,
5102    /// Number of passed steps.
5103    pub passed: usize,
5104    /// Number of failed steps.
5105    pub failed: usize,
5106    /// Number of skipped steps.
5107    pub skipped: usize,
5108    /// Total duration.
5109    pub total_duration: std::time::Duration,
5110    /// Step details.
5111    pub steps: Vec<E2EStep>,
5112    /// Log messages.
5113    pub logs: Vec<String>,
5114}
5115
5116impl E2EReport {
5117    /// Renders the report as plain text.
5118    #[must_use]
5119    pub fn to_text(&self) -> String {
5120        let mut output = String::new();
5121
5122        // Header
5123        output.push_str(&format!("E2E Test Report: {}\n", self.scenario_name));
5124        output.push_str(&"=".repeat(60));
5125        output.push('\n');
5126
5127        if let Some(desc) = &self.description {
5128            output.push_str(&format!("Description: {}\n", desc));
5129        }
5130
5131        // Summary
5132        output.push_str(&format!(
5133            "\nSummary: {} passed, {} failed, {} skipped\n",
5134            self.passed, self.failed, self.skipped
5135        ));
5136        output.push_str(&format!("Total Duration: {:?}\n", self.total_duration));
5137        output.push_str(&"-".repeat(60));
5138        output.push('\n');
5139
5140        // Steps
5141        output.push_str("\nSteps:\n");
5142        for (i, step) in self.steps.iter().enumerate() {
5143            let status = match &step.result {
5144                E2EStepResult::Passed => "[PASS]",
5145                E2EStepResult::Failed(_) => "[FAIL]",
5146                E2EStepResult::Skipped => "[SKIP]",
5147            };
5148            output.push_str(&format!(
5149                "  {}. {} {} ({:?})\n",
5150                i + 1,
5151                status,
5152                step.name,
5153                step.duration
5154            ));
5155            if let E2EStepResult::Failed(msg) = &step.result {
5156                output.push_str(&format!("     Error: {}\n", msg));
5157            }
5158        }
5159
5160        // Logs
5161        if !self.logs.is_empty() {
5162            output.push_str(&"-".repeat(60));
5163            output.push_str("\n\nLogs:\n");
5164            for log in &self.logs {
5165                output.push_str(&format!("  {}\n", log));
5166            }
5167        }
5168
5169        output
5170    }
5171
5172    /// Renders the report as JSON.
5173    #[must_use]
5174    pub fn to_json(&self) -> String {
5175        let steps_json: Vec<String> = self
5176            .steps
5177            .iter()
5178            .map(|step| {
5179                let status = match &step.result {
5180                    E2EStepResult::Passed => "passed",
5181                    E2EStepResult::Failed(_) => "failed",
5182                    E2EStepResult::Skipped => "skipped",
5183                };
5184                let error = match &step.result {
5185                    E2EStepResult::Failed(msg) => format!(r#", "error": "{}""#, escape_json(msg)),
5186                    _ => String::new(),
5187                };
5188                format!(
5189                    r#"    {{ "name": "{}", "status": "{}", "duration_ms": {}{} }}"#,
5190                    escape_json(&step.name),
5191                    status,
5192                    step.duration.as_millis(),
5193                    error
5194                )
5195            })
5196            .collect();
5197
5198        format!(
5199            r#"{{
5200  "scenario": "{}",
5201  "description": {},
5202  "summary": {{
5203    "passed": {},
5204    "failed": {},
5205    "skipped": {},
5206    "total_duration_ms": {}
5207  }},
5208  "steps": [
5209{}
5210  ]
5211}}"#,
5212            escape_json(&self.scenario_name),
5213            self.description
5214                .as_ref()
5215                .map_or("null".to_string(), |d| format!(r#""{}""#, escape_json(d))),
5216            self.passed,
5217            self.failed,
5218            self.skipped,
5219            self.total_duration.as_millis(),
5220            steps_json.join(",\n")
5221        )
5222    }
5223
5224    /// Renders the report as HTML.
5225    #[must_use]
5226    pub fn to_html(&self) -> String {
5227        let status_class = if self.failed > 0 { "failed" } else { "passed" };
5228
5229        use std::fmt::Write;
5230        let steps_html =
5231            self.steps
5232                .iter()
5233                .enumerate()
5234                .fold(String::new(), |mut output, (i, step)| {
5235                    let (status, class) = match &step.result {
5236                        E2EStepResult::Passed => ("✓", "pass"),
5237                        E2EStepResult::Failed(_) => ("✗", "fail"),
5238                        E2EStepResult::Skipped => ("○", "skip"),
5239                    };
5240                    let error_html = match &step.result {
5241                        E2EStepResult::Failed(msg) => {
5242                            format!(r#"<div class="error">{}</div>"#, escape_html(msg))
5243                        }
5244                        _ => String::new(),
5245                    };
5246                    let _ = write!(
5247                        output,
5248                        r#"    <tr class="{}">
5249      <td>{}</td>
5250      <td><span class="status">{}</span></td>
5251      <td>{}</td>
5252      <td>{:?}</td>
5253    </tr>
5254    {}"#,
5255                        class,
5256                        i + 1,
5257                        status,
5258                        escape_html(&step.name),
5259                        step.duration,
5260                        error_html
5261                    );
5262                    output
5263                });
5264
5265        format!(
5266            r#"<!DOCTYPE html>
5267<html>
5268<head>
5269  <title>E2E Report: {}</title>
5270  <style>
5271    body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 2rem; }}
5272    h1 {{ color: #333; }}
5273    .summary {{ padding: 1rem; border-radius: 8px; margin: 1rem 0; }}
5274    .summary.passed {{ background: #d4edda; }}
5275    .summary.failed {{ background: #f8d7da; }}
5276    table {{ width: 100%; border-collapse: collapse; margin-top: 1rem; }}
5277    th, td {{ padding: 0.75rem; text-align: left; border-bottom: 1px solid #dee2e6; }}
5278    th {{ background: #f8f9fa; }}
5279    .pass {{ color: #28a745; }}
5280    .fail {{ color: #dc3545; }}
5281    .skip {{ color: #6c757d; }}
5282    .status {{ font-size: 1.2rem; }}
5283    .error {{ color: #dc3545; font-size: 0.9rem; padding: 0.5rem; background: #fff; margin-top: 0.25rem; }}
5284  </style>
5285</head>
5286<body>
5287  <h1>E2E Report: {}</h1>
5288  {}
5289  <div class="summary {}">
5290    <strong>Summary:</strong> {} passed, {} failed, {} skipped<br>
5291    <strong>Duration:</strong> {:?}
5292  </div>
5293  <table>
5294    <thead>
5295      <tr><th>#</th><th>Status</th><th>Step</th><th>Duration</th></tr>
5296    </thead>
5297    <tbody>
5298{}
5299    </tbody>
5300  </table>
5301</body>
5302</html>"#,
5303            escape_html(&self.scenario_name),
5304            escape_html(&self.scenario_name),
5305            self.description
5306                .as_ref()
5307                .map_or(String::new(), |d| format!("<p>{}</p>", escape_html(d))),
5308            status_class,
5309            self.passed,
5310            self.failed,
5311            self.skipped,
5312            self.total_duration,
5313            steps_html
5314        )
5315    }
5316}
5317
5318/// Helper function to escape JSON strings.
5319fn escape_json(s: &str) -> String {
5320    s.replace('\\', "\\\\")
5321        .replace('"', "\\\"")
5322        .replace('\n', "\\n")
5323        .replace('\r', "\\r")
5324        .replace('\t', "\\t")
5325}
5326
5327/// Helper function to escape HTML.
5328fn escape_html(s: &str) -> String {
5329    s.replace('&', "&amp;")
5330        .replace('<', "&lt;")
5331        .replace('>', "&gt;")
5332        .replace('"', "&quot;")
5333}
5334
5335/// Macro for defining E2E test scenarios with a declarative syntax.
5336///
5337/// # Example
5338///
5339/// ```ignore
5340/// use fastapi_core::testing::{e2e_test, TestClient};
5341///
5342/// e2e_test! {
5343///     name: "User Login Flow",
5344///     description: "Tests the complete user login process",
5345///     client: TestClient::new(app),
5346///
5347///     step "Navigate to login page" => |client| {
5348///         let response = client.get("/login").send();
5349///         assert_eq!(response.status().as_u16(), 200);
5350///     },
5351///
5352///     step "Submit credentials" => |client| {
5353///         let response = client
5354///             .post("/login")
5355///             .json(&serde_json::json!({"username": "test", "password": "secret"}))
5356///             .send();
5357///         assert_eq!(response.status().as_u16(), 302);
5358///     },
5359///
5360///     step "Access dashboard" => |client| {
5361///         let response = client.get("/dashboard").send();
5362///         assert_eq!(response.status().as_u16(), 200);
5363///         assert!(response.text().contains("Welcome"));
5364///     },
5365/// }
5366/// ```
5367#[macro_export]
5368macro_rules! e2e_test {
5369    (
5370        name: $name:expr,
5371        $(description: $desc:expr,)?
5372        client: $client:expr,
5373        $(step $step_name:literal => |$client_param:ident| $step_body:block),+ $(,)?
5374    ) => {{
5375        let client = $client;
5376        let mut scenario = $crate::testing::E2EScenario::new($name, client);
5377        $(
5378            scenario = scenario.description($desc);
5379        )?
5380        $(
5381            scenario.step($step_name, |$client_param| $step_body);
5382        )+
5383        scenario.assert_passed();
5384        scenario.report()
5385    }};
5386}
5387
5388pub use e2e_test;
5389
5390// =============================================================================
5391// Test Logging Utilities
5392// =============================================================================
5393
5394use crate::logging::{LogEntry, LogLevel};
5395
5396/// A captured log entry for test assertions.
5397#[derive(Debug, Clone)]
5398pub struct CapturedLog {
5399    /// The log level.
5400    pub level: LogLevel,
5401    /// The log message.
5402    pub message: String,
5403    /// Request ID associated with this log.
5404    pub request_id: u64,
5405    /// Timestamp when captured.
5406    pub captured_at: std::time::Instant,
5407    /// Structured fields as key-value pairs.
5408    pub fields: Vec<(String, String)>,
5409    /// Target module path (if any).
5410    pub target: Option<String>,
5411}
5412
5413impl CapturedLog {
5414    /// Creates a new captured log from a LogEntry.
5415    pub fn from_entry(entry: &LogEntry) -> Self {
5416        Self {
5417            level: entry.level,
5418            message: entry.message.clone(),
5419            request_id: entry.request_id,
5420            captured_at: std::time::Instant::now(),
5421            fields: entry.fields.clone(),
5422            target: entry.target.clone(),
5423        }
5424    }
5425
5426    /// Creates a captured log directly with specified values.
5427    pub fn new(level: LogLevel, message: impl Into<String>, request_id: u64) -> Self {
5428        Self {
5429            level,
5430            message: message.into(),
5431            request_id,
5432            captured_at: std::time::Instant::now(),
5433            fields: Vec::new(),
5434            target: None,
5435        }
5436    }
5437
5438    /// Checks if the message contains the given substring.
5439    #[must_use]
5440    pub fn contains(&self, text: &str) -> bool {
5441        self.message.contains(text)
5442    }
5443
5444    /// Formats for display in test output.
5445    #[must_use]
5446    pub fn format(&self) -> String {
5447        let mut output = format!(
5448            "[{}] req={} {}",
5449            self.level.as_char(),
5450            self.request_id,
5451            self.message
5452        );
5453        if !self.fields.is_empty() {
5454            output.push_str(" {");
5455            for (i, (k, v)) in self.fields.iter().enumerate() {
5456                if i > 0 {
5457                    output.push_str(", ");
5458                }
5459                output.push_str(&format!("{k}={v}"));
5460            }
5461            output.push('}');
5462        }
5463        output
5464    }
5465}
5466
5467/// Test logger that captures logs for per-test isolation and assertions.
5468///
5469/// Use `TestLogger::capture` to run a test with isolated log capture,
5470/// then examine captured logs for assertions.
5471///
5472/// # Example
5473///
5474/// ```ignore
5475/// use fastapi_core::testing::TestLogger;
5476/// use fastapi_core::logging::LogLevel;
5477///
5478/// let capture = TestLogger::capture(|| {
5479///     let ctx = RequestContext::for_testing();
5480///     log_info!(ctx, "Hello from test");
5481///     log_debug!(ctx, "Debug info");
5482/// });
5483///
5484/// // Assert on captured logs
5485/// assert!(capture.contains_message("Hello from test"));
5486/// assert_eq!(capture.count_by_level(LogLevel::Info), 1);
5487///
5488/// // Get failure context (last N logs)
5489/// let context = capture.failure_context(5);
5490/// ```
5491#[derive(Debug, Clone)]
5492pub struct TestLogger {
5493    /// Captured log entries.
5494    logs: Arc<Mutex<Vec<CapturedLog>>>,
5495    /// Test phase timings.
5496    timings: Arc<Mutex<TestTimings>>,
5497    /// Whether to echo logs to stderr (for debugging).
5498    echo_logs: bool,
5499}
5500
5501/// Timing breakdown for test phases.
5502#[derive(Debug, Clone, Default)]
5503pub struct TestTimings {
5504    /// Setup phase duration.
5505    pub setup: Option<std::time::Duration>,
5506    /// Execute phase duration.
5507    pub execute: Option<std::time::Duration>,
5508    /// Teardown phase duration.
5509    pub teardown: Option<std::time::Duration>,
5510    /// Phase start time.
5511    phase_start: Option<std::time::Instant>,
5512}
5513
5514impl TestTimings {
5515    /// Starts timing a phase.
5516    pub fn start_phase(&mut self) {
5517        self.phase_start = Some(std::time::Instant::now());
5518    }
5519
5520    /// Ends the setup phase.
5521    pub fn end_setup(&mut self) {
5522        if let Some(start) = self.phase_start.take() {
5523            self.setup = Some(start.elapsed());
5524        }
5525    }
5526
5527    /// Ends the execute phase.
5528    pub fn end_execute(&mut self) {
5529        if let Some(start) = self.phase_start.take() {
5530            self.execute = Some(start.elapsed());
5531        }
5532    }
5533
5534    /// Ends the teardown phase.
5535    pub fn end_teardown(&mut self) {
5536        if let Some(start) = self.phase_start.take() {
5537            self.teardown = Some(start.elapsed());
5538        }
5539    }
5540
5541    /// Total test duration.
5542    #[must_use]
5543    pub fn total(&self) -> std::time::Duration {
5544        self.setup.unwrap_or_default()
5545            + self.execute.unwrap_or_default()
5546            + self.teardown.unwrap_or_default()
5547    }
5548
5549    /// Formats timings for display.
5550    #[must_use]
5551    pub fn format(&self) -> String {
5552        format!(
5553            "Timings: setup={:?}, execute={:?}, teardown={:?}, total={:?}",
5554            self.setup.unwrap_or_default(),
5555            self.execute.unwrap_or_default(),
5556            self.teardown.unwrap_or_default(),
5557            self.total()
5558        )
5559    }
5560}
5561
5562impl TestLogger {
5563    /// Creates a new test logger.
5564    pub fn new() -> Self {
5565        Self {
5566            logs: Arc::new(Mutex::new(Vec::new())),
5567            timings: Arc::new(Mutex::new(TestTimings::default())),
5568            echo_logs: std::env::var("FASTAPI_TEST_ECHO_LOGS").is_ok(),
5569        }
5570    }
5571
5572    /// Creates a logger that echoes logs to stderr.
5573    pub fn with_echo() -> Self {
5574        let mut logger = Self::new();
5575        logger.echo_logs = true;
5576        logger
5577    }
5578
5579    /// Captures a log entry.
5580    pub fn log(&self, entry: CapturedLog) {
5581        if self.echo_logs {
5582            eprintln!("[LOG] {}", entry.format());
5583        }
5584        self.logs.lock().push(entry);
5585    }
5586
5587    /// Captures a log from a LogEntry.
5588    pub fn log_entry(&self, entry: &LogEntry) {
5589        self.log(CapturedLog::from_entry(entry));
5590    }
5591
5592    /// Logs a message directly (convenience method).
5593    pub fn log_message(&self, level: LogLevel, message: impl Into<String>, request_id: u64) {
5594        self.log(CapturedLog::new(level, message, request_id));
5595    }
5596
5597    /// Gets all captured logs.
5598    #[must_use]
5599    pub fn logs(&self) -> Vec<CapturedLog> {
5600        self.logs.lock().clone()
5601    }
5602
5603    /// Gets the number of captured logs.
5604    #[must_use]
5605    pub fn count(&self) -> usize {
5606        self.logs.lock().len()
5607    }
5608
5609    /// Clears all captured logs.
5610    pub fn clear(&self) {
5611        self.logs.lock().clear();
5612    }
5613
5614    /// Checks if any log contains the given message substring.
5615    #[must_use]
5616    pub fn contains_message(&self, text: &str) -> bool {
5617        self.logs.lock().iter().any(|log| log.contains(text))
5618    }
5619
5620    /// Counts logs by level.
5621    #[must_use]
5622    pub fn count_by_level(&self, level: LogLevel) -> usize {
5623        self.logs
5624            .lock()
5625            .iter()
5626            .filter(|log| log.level == level)
5627            .count()
5628    }
5629
5630    /// Gets logs at a specific level.
5631    #[must_use]
5632    pub fn logs_at_level(&self, level: LogLevel) -> Vec<CapturedLog> {
5633        self.logs
5634            .lock()
5635            .iter()
5636            .filter(|log| log.level == level)
5637            .cloned()
5638            .collect()
5639    }
5640
5641    /// Gets the last N logs for failure context.
5642    #[must_use]
5643    pub fn failure_context(&self, n: usize) -> String {
5644        let logs = self.logs.lock();
5645        let start = logs.len().saturating_sub(n);
5646        let recent: Vec<_> = logs[start..].iter().map(CapturedLog::format).collect();
5647
5648        if recent.is_empty() {
5649            "No logs captured".to_string()
5650        } else {
5651            format!(
5652                "Last {} log(s) before failure:\n  {}",
5653                recent.len(),
5654                recent.join("\n  ")
5655            )
5656        }
5657    }
5658
5659    /// Gets timing breakdown.
5660    #[must_use]
5661    pub fn timings(&self) -> TestTimings {
5662        self.timings.lock().clone()
5663    }
5664
5665    /// Starts timing a phase.
5666    pub fn start_phase(&self) {
5667        self.timings.lock().start_phase();
5668    }
5669
5670    /// Marks end of setup phase.
5671    pub fn end_setup(&self) {
5672        self.timings.lock().end_setup();
5673    }
5674
5675    /// Marks end of execute phase.
5676    pub fn end_execute(&self) {
5677        self.timings.lock().end_execute();
5678    }
5679
5680    /// Marks end of teardown phase.
5681    pub fn end_teardown(&self) {
5682        self.timings.lock().end_teardown();
5683    }
5684
5685    /// Runs a closure with log capture, returning a LogCapture result.
5686    ///
5687    /// This is the primary API for isolated test logging.
5688    pub fn capture<F, T>(f: F) -> LogCapture<T>
5689    where
5690        F: FnOnce(&TestLogger) -> T,
5691    {
5692        let logger = TestLogger::new();
5693
5694        logger.start_phase();
5695        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5696            logger.end_setup();
5697            logger.start_phase();
5698            let result = f(&logger);
5699            logger.end_execute();
5700            result
5701        }));
5702
5703        let (ok_result, panic_info) = match result {
5704            Ok(v) => (Some(v), None),
5705            Err(p) => {
5706                let msg = if let Some(s) = p.downcast_ref::<&str>() {
5707                    (*s).to_string()
5708                } else if let Some(s) = p.downcast_ref::<String>() {
5709                    s.clone()
5710                } else {
5711                    "Unknown panic".to_string()
5712                };
5713                (None, Some(msg))
5714            }
5715        };
5716
5717        LogCapture {
5718            logs: logger.logs(),
5719            timings: logger.timings(),
5720            result: ok_result,
5721            panic_info,
5722        }
5723    }
5724
5725    /// Runs a test with setup, execute, and teardown phases.
5726    pub fn capture_phased<S, E, D, T>(setup: S, execute: E, teardown: D) -> LogCapture<T>
5727    where
5728        S: FnOnce(&TestLogger),
5729        E: FnOnce(&TestLogger) -> T,
5730        D: FnOnce(&TestLogger),
5731    {
5732        let logger = TestLogger::new();
5733
5734        // Setup phase
5735        logger.start_phase();
5736        let setup_panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5737            setup(&logger);
5738        }));
5739        logger.end_setup();
5740
5741        if setup_panic.is_err() {
5742            return LogCapture {
5743                logs: logger.logs(),
5744                timings: logger.timings(),
5745                result: None,
5746                panic_info: Some("Setup phase panicked".to_string()),
5747            };
5748        }
5749
5750        // Execute phase
5751        logger.start_phase();
5752        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| execute(&logger)));
5753        logger.end_execute();
5754
5755        // Teardown phase (always runs)
5756        logger.start_phase();
5757        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5758            teardown(&logger);
5759        }));
5760        logger.end_teardown();
5761
5762        let (ok_result, panic_info) = match result {
5763            Ok(v) => (Some(v), None),
5764            Err(p) => {
5765                let msg = if let Some(s) = p.downcast_ref::<&str>() {
5766                    (*s).to_string()
5767                } else if let Some(s) = p.downcast_ref::<String>() {
5768                    s.clone()
5769                } else {
5770                    "Unknown panic".to_string()
5771                };
5772                (None, Some(msg))
5773            }
5774        };
5775
5776        LogCapture {
5777            logs: logger.logs(),
5778            timings: logger.timings(),
5779            result: ok_result,
5780            panic_info,
5781        }
5782    }
5783}
5784
5785impl Default for TestLogger {
5786    fn default() -> Self {
5787        Self::new()
5788    }
5789}
5790
5791/// Result of a log capture operation.
5792#[derive(Debug)]
5793pub struct LogCapture<T> {
5794    /// Captured log entries.
5795    pub logs: Vec<CapturedLog>,
5796    /// Phase timings.
5797    pub timings: TestTimings,
5798    /// Test result (if successful).
5799    pub result: Option<T>,
5800    /// Panic information (if failed).
5801    pub panic_info: Option<String>,
5802}
5803
5804impl<T> LogCapture<T> {
5805    /// Returns `true` if the test passed.
5806    #[must_use]
5807    pub fn passed(&self) -> bool {
5808        self.result.is_some()
5809    }
5810
5811    /// Returns `true` if the test failed.
5812    #[must_use]
5813    pub fn failed(&self) -> bool {
5814        self.panic_info.is_some()
5815    }
5816
5817    /// Checks if any log contains the given message substring.
5818    #[must_use]
5819    pub fn contains_message(&self, text: &str) -> bool {
5820        self.logs.iter().any(|log| log.contains(text))
5821    }
5822
5823    /// Counts logs by level.
5824    #[must_use]
5825    pub fn count_by_level(&self, level: LogLevel) -> usize {
5826        self.logs.iter().filter(|log| log.level == level).count()
5827    }
5828
5829    /// Gets the last N logs for failure context.
5830    #[must_use]
5831    pub fn failure_context(&self, n: usize) -> String {
5832        let start = self.logs.len().saturating_sub(n);
5833        let recent: Vec<_> = self.logs[start..].iter().map(CapturedLog::format).collect();
5834
5835        let mut output = String::new();
5836
5837        if let Some(ref panic) = self.panic_info {
5838            output.push_str(&format!("Test failed: {}\n\n", panic));
5839        }
5840
5841        output.push_str(&self.timings.format());
5842        output.push_str("\n\n");
5843
5844        if recent.is_empty() {
5845            output.push_str("No logs captured");
5846        } else {
5847            output.push_str(&format!(
5848                "Last {} log(s) before failure:\n  {}",
5849                recent.len(),
5850                recent.join("\n  ")
5851            ));
5852        }
5853
5854        output
5855    }
5856
5857    /// Unwraps the result, panicking with failure context if it failed.
5858    pub fn unwrap(self) -> T {
5859        match self.result {
5860            Some(v) => v,
5861            None => panic!(
5862                "Test failed with log context:\n{}",
5863                self.failure_context(10)
5864            ),
5865        }
5866    }
5867
5868    /// Gets the result or returns a default.
5869    pub fn unwrap_or(self, default: T) -> T {
5870        self.result.unwrap_or(default)
5871    }
5872}
5873
5874/// Assertion helper that includes log context on failure.
5875///
5876/// Use this instead of `assert!` to automatically include recent logs in
5877/// the failure message.
5878#[macro_export]
5879macro_rules! assert_with_logs {
5880    ($logger:expr, $cond:expr) => {
5881        if !$cond {
5882            panic!(
5883                "Assertion failed: {}\n\n{}",
5884                stringify!($cond),
5885                $logger.failure_context(10)
5886            );
5887        }
5888    };
5889    ($logger:expr, $cond:expr, $($arg:tt)+) => {
5890        if !$cond {
5891            panic!(
5892                "Assertion failed: {}\n\n{}",
5893                format!($($arg)+),
5894                $logger.failure_context(10)
5895            );
5896        }
5897    };
5898}
5899
5900/// Assertion helper that includes log context for equality checks.
5901#[macro_export]
5902macro_rules! assert_eq_with_logs {
5903    ($logger:expr, $left:expr, $right:expr) => {
5904        if $left != $right {
5905            panic!(
5906                "Assertion failed: {} == {}\n  left:  {:?}\n  right: {:?}\n\n{}",
5907                stringify!($left),
5908                stringify!($right),
5909                $left,
5910                $right,
5911                $logger.failure_context(10)
5912            );
5913        }
5914    };
5915    ($logger:expr, $left:expr, $right:expr, $($arg:tt)+) => {
5916        if $left != $right {
5917            panic!(
5918                "Assertion failed: {}\n  left:  {:?}\n  right: {:?}\n\n{}",
5919                format!($($arg)+),
5920                $left,
5921                $right,
5922                $logger.failure_context(10)
5923            );
5924        }
5925    };
5926}
5927
5928pub use assert_eq_with_logs;
5929pub use assert_with_logs;
5930
5931/// Request/response diff helper for test assertions.
5932#[derive(Debug)]
5933pub struct ResponseDiff {
5934    /// Expected status code.
5935    pub expected_status: u16,
5936    /// Actual status code.
5937    pub actual_status: u16,
5938    /// Expected body substring or full content.
5939    pub expected_body: Option<String>,
5940    /// Actual body content.
5941    pub actual_body: String,
5942    /// Header differences (name, expected, actual).
5943    pub header_diffs: Vec<(String, Option<String>, Option<String>)>,
5944}
5945
5946impl ResponseDiff {
5947    /// Creates a new diff from expected and actual responses.
5948    pub fn new(expected_status: u16, actual: &TestResponse) -> Self {
5949        Self {
5950            expected_status,
5951            actual_status: actual.status().as_u16(),
5952            expected_body: None,
5953            actual_body: actual.text().to_string(),
5954            header_diffs: Vec::new(),
5955        }
5956    }
5957
5958    /// Sets expected body for comparison.
5959    #[must_use]
5960    pub fn expected_body(mut self, body: impl Into<String>) -> Self {
5961        self.expected_body = Some(body.into());
5962        self
5963    }
5964
5965    /// Adds an expected header.
5966    #[must_use]
5967    pub fn expected_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
5968        self.header_diffs
5969            .push((name.into(), Some(value.into()), None));
5970        self
5971    }
5972
5973    /// Returns `true` if there are no differences.
5974    #[must_use]
5975    pub fn is_match(&self) -> bool {
5976        if self.expected_status != self.actual_status {
5977            return false;
5978        }
5979        if let Some(ref expected) = self.expected_body {
5980            if !self.actual_body.contains(expected) {
5981                return false;
5982            }
5983        }
5984        true
5985    }
5986
5987    /// Formats the diff for display.
5988    #[must_use]
5989    pub fn format(&self) -> String {
5990        let mut output = String::new();
5991
5992        if self.expected_status != self.actual_status {
5993            output.push_str(&format!(
5994                "Status mismatch:\n  expected: {}\n  actual:   {}\n",
5995                self.expected_status, self.actual_status
5996            ));
5997        }
5998
5999        if let Some(ref expected) = self.expected_body {
6000            if !self.actual_body.contains(expected) {
6001                output.push_str(&format!(
6002                    "Body mismatch:\n  expected to contain: {:?}\n  actual: {:?}\n",
6003                    expected, self.actual_body
6004                ));
6005            }
6006        }
6007
6008        for (name, expected, actual) in &self.header_diffs {
6009            output.push_str(&format!(
6010                "Header '{}' mismatch:\n  expected: {:?}\n  actual:   {:?}\n",
6011                name, expected, actual
6012            ));
6013        }
6014
6015        if output.is_empty() {
6016            "No differences".to_string()
6017        } else {
6018            output
6019        }
6020    }
6021}
6022
6023// ============================================================================
6024// Snapshot Testing Utilities
6025// ============================================================================
6026
6027/// A serializable snapshot of an HTTP response for fixture-based testing.
6028///
6029/// Snapshots capture status code, selected headers, and body content,
6030/// enabling API contract verification by comparing responses against
6031/// stored fixtures.
6032///
6033/// # Usage
6034///
6035/// ```ignore
6036/// let response = client.get("/api/users").send();
6037/// let snapshot = ResponseSnapshot::from_test_response(&response);
6038///
6039/// // First run: save the snapshot
6040/// snapshot.save("tests/snapshots/get_users.json").unwrap();
6041///
6042/// // Subsequent runs: compare against saved snapshot
6043/// let expected = ResponseSnapshot::load("tests/snapshots/get_users.json").unwrap();
6044/// assert_eq!(snapshot, expected, "{}", snapshot.diff(&expected));
6045/// ```
6046#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
6047pub struct ResponseSnapshot {
6048    /// HTTP status code.
6049    pub status: u16,
6050    /// Selected response headers (name, value) — sorted for determinism.
6051    pub headers: Vec<(String, String)>,
6052    /// Response body as a string.
6053    pub body: String,
6054    /// If the body is valid JSON, the parsed value for structural comparison.
6055    #[serde(skip_serializing_if = "Option::is_none")]
6056    pub body_json: Option<serde_json::Value>,
6057}
6058
6059impl ResponseSnapshot {
6060    /// Create a snapshot from a `TestResponse`.
6061    ///
6062    /// Captures the status code, all response headers, and body text.
6063    /// If the body is valid JSON, it's also parsed for structural comparison.
6064    pub fn from_test_response(resp: &TestResponse) -> Self {
6065        let body = resp.text().to_string();
6066        let body_json = serde_json::from_str::<serde_json::Value>(&body).ok();
6067
6068        let mut headers: Vec<(String, String)> = resp
6069            .headers()
6070            .iter()
6071            .filter_map(|(name, value)| {
6072                std::str::from_utf8(value)
6073                    .ok()
6074                    .map(|v| (name.to_lowercase(), v.to_string()))
6075            })
6076            .collect();
6077        headers.sort();
6078
6079        Self {
6080            status: resp.status().as_u16(),
6081            headers,
6082            body,
6083            body_json,
6084        }
6085    }
6086
6087    /// Create a snapshot with only specific headers (for ignoring dynamic headers).
6088    pub fn from_test_response_with_headers(resp: &TestResponse, header_names: &[&str]) -> Self {
6089        let mut snapshot = Self::from_test_response(resp);
6090        let names: Vec<String> = header_names.iter().map(|n| n.to_lowercase()).collect();
6091        snapshot.headers.retain(|(name, _)| names.contains(name));
6092        snapshot
6093    }
6094
6095    /// Mask dynamic fields in the JSON body (replace with a placeholder).
6096    ///
6097    /// This is useful for fields like timestamps, UUIDs, or auto-increment IDs
6098    /// that change between test runs.
6099    ///
6100    /// `paths` are dot-separated JSON paths, e.g. `["id", "created_at", "items.0.id"]`.
6101    #[must_use]
6102    pub fn mask_fields(mut self, paths: &[&str], placeholder: &str) -> Self {
6103        if let Some(ref mut json) = self.body_json {
6104            for path in paths {
6105                mask_json_path(json, path, placeholder);
6106            }
6107            self.body = serde_json::to_string_pretty(json).unwrap_or(self.body);
6108        }
6109        self
6110    }
6111
6112    /// Save the snapshot to a JSON file.
6113    pub fn save(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
6114        let path = path.as_ref();
6115        if let Some(parent) = path.parent() {
6116            std::fs::create_dir_all(parent)?;
6117        }
6118        let json = serde_json::to_string_pretty(self).map_err(std::io::Error::other)?;
6119        std::fs::write(path, json)
6120    }
6121
6122    /// Load a snapshot from a JSON file.
6123    pub fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
6124        let data = std::fs::read_to_string(path)?;
6125        serde_json::from_str(&data)
6126            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
6127    }
6128
6129    /// Compare two snapshots and return a human-readable diff.
6130    #[must_use]
6131    pub fn diff(&self, other: &Self) -> String {
6132        let mut output = String::new();
6133
6134        if self.status != other.status {
6135            output.push_str(&format!("Status: {} vs {}\n", self.status, other.status));
6136        }
6137
6138        // Header diffs
6139        for (name, value) in &self.headers {
6140            match other.headers.iter().find(|(n, _)| n == name) {
6141                Some((_, other_value)) if value != other_value => {
6142                    output.push_str(&format!(
6143                        "Header '{}': {:?} vs {:?}\n",
6144                        name, value, other_value
6145                    ));
6146                }
6147                None => {
6148                    output.push_str(&format!("Header '{}': present vs missing\n", name));
6149                }
6150                _ => {}
6151            }
6152        }
6153        for (name, _) in &other.headers {
6154            if !self.headers.iter().any(|(n, _)| n == name) {
6155                output.push_str(&format!("Header '{}': missing vs present\n", name));
6156            }
6157        }
6158
6159        // Body diff
6160        if self.body != other.body {
6161            output.push_str(&format!(
6162                "Body:\n  expected: {:?}\n  actual:   {:?}\n",
6163                other.body, self.body
6164            ));
6165        }
6166
6167        if output.is_empty() {
6168            "No differences".to_string()
6169        } else {
6170            output
6171        }
6172    }
6173
6174    /// Check if two snapshots match, optionally ignoring specific headers.
6175    pub fn matches_ignoring_headers(&self, other: &Self, ignore: &[&str]) -> bool {
6176        if self.status != other.status {
6177            return false;
6178        }
6179
6180        let ignore_lower: Vec<String> = ignore.iter().map(|s| s.to_lowercase()).collect();
6181
6182        let self_headers: Vec<_> = self
6183            .headers
6184            .iter()
6185            .filter(|(n, _)| !ignore_lower.contains(n))
6186            .collect();
6187        let other_headers: Vec<_> = other
6188            .headers
6189            .iter()
6190            .filter(|(n, _)| !ignore_lower.contains(n))
6191            .collect();
6192
6193        if self_headers != other_headers {
6194            return false;
6195        }
6196
6197        // Compare JSON structurally if available, else compare strings
6198        match (&self.body_json, &other.body_json) {
6199            (Some(a), Some(b)) => a == b,
6200            _ => self.body == other.body,
6201        }
6202    }
6203}
6204
6205/// Helper to mask a value at a dot-separated JSON path.
6206fn mask_json_path(value: &mut serde_json::Value, path: &str, placeholder: &str) {
6207    let parts: Vec<&str> = path.splitn(2, '.').collect();
6208    match parts.as_slice() {
6209        [key] => {
6210            if let Some(obj) = value.as_object_mut() {
6211                if obj.contains_key(*key) {
6212                    obj.insert(
6213                        key.to_string(),
6214                        serde_json::Value::String(placeholder.to_string()),
6215                    );
6216                }
6217            }
6218            if let Some(arr) = value.as_array_mut() {
6219                if let Ok(idx) = key.parse::<usize>() {
6220                    if idx < arr.len() {
6221                        arr[idx] = serde_json::Value::String(placeholder.to_string());
6222                    }
6223                }
6224            }
6225        }
6226        [key, rest] => {
6227            if let Some(obj) = value.as_object_mut() {
6228                if let Some(child) = obj.get_mut(*key) {
6229                    mask_json_path(child, rest, placeholder);
6230                }
6231            }
6232            if let Some(arr) = value.as_array_mut() {
6233                if let Ok(idx) = key.parse::<usize>() {
6234                    if let Some(child) = arr.get_mut(idx) {
6235                        mask_json_path(child, rest, placeholder);
6236                    }
6237                }
6238            }
6239        }
6240        _ => {}
6241    }
6242}
6243
6244/// Macro for snapshot testing a response against a file fixture.
6245///
6246/// On first run (or when `SNAPSHOT_UPDATE=1`), saves the snapshot.
6247/// On subsequent runs, compares against the saved snapshot.
6248///
6249/// # Usage
6250///
6251/// ```ignore
6252/// let response = client.get("/api/users").send();
6253/// assert_response_snapshot!(response, "tests/snapshots/get_users.json");
6254///
6255/// // With field masking:
6256/// assert_response_snapshot!(response, "tests/snapshots/get_users.json", mask: ["id", "created_at"]);
6257/// ```
6258#[macro_export]
6259macro_rules! assert_response_snapshot {
6260    ($response:expr, $path:expr) => {{
6261        let snapshot = $crate::ResponseSnapshot::from_test_response(&$response);
6262        let path = std::path::Path::new($path);
6263
6264        if std::env::var("SNAPSHOT_UPDATE").is_ok() || !path.exists() {
6265            snapshot.save(path).expect("failed to save snapshot");
6266        } else {
6267            let expected =
6268                $crate::ResponseSnapshot::load(path).expect("failed to load snapshot");
6269            assert!(
6270                snapshot == expected,
6271                "Snapshot mismatch for {}:\n{}",
6272                $path,
6273                snapshot.diff(&expected)
6274            );
6275        }
6276    }};
6277    ($response:expr, $path:expr, mask: [$($field:expr),* $(,)?]) => {{
6278        let snapshot = $crate::ResponseSnapshot::from_test_response(&$response)
6279            .mask_fields(&[$($field),*], "<MASKED>");
6280        let path = std::path::Path::new($path);
6281
6282        if std::env::var("SNAPSHOT_UPDATE").is_ok() || !path.exists() {
6283            snapshot.save(path).expect("failed to save snapshot");
6284        } else {
6285            let expected =
6286                $crate::ResponseSnapshot::load(path).expect("failed to load snapshot");
6287            assert!(
6288                snapshot == expected,
6289                "Snapshot mismatch for {}:\n{}",
6290                $path,
6291                snapshot.diff(&expected)
6292            );
6293        }
6294    }};
6295}
6296
6297#[cfg(test)]
6298mod snapshot_tests {
6299    use super::*;
6300
6301    fn mock_test_response(status: u16, body: &str, headers: &[(&str, &str)]) -> TestResponse {
6302        let mut resp =
6303            crate::response::Response::with_status(crate::response::StatusCode::from_u16(status));
6304        for (name, value) in headers {
6305            resp = resp.header(*name, value.as_bytes().to_vec());
6306        }
6307        resp = resp.body(crate::response::ResponseBody::Bytes(
6308            body.as_bytes().to_vec(),
6309        ));
6310        TestResponse::new(resp, 0)
6311    }
6312
6313    #[test]
6314    fn snapshot_from_test_response() {
6315        let resp = mock_test_response(
6316            200,
6317            r#"{"id":1,"name":"Alice"}"#,
6318            &[("content-type", "application/json")],
6319        );
6320        let snap = ResponseSnapshot::from_test_response(&resp);
6321
6322        assert_eq!(snap.status, 200);
6323        assert!(snap.body_json.is_some());
6324        assert_eq!(snap.body_json.as_ref().unwrap()["name"], "Alice");
6325    }
6326
6327    #[test]
6328    fn snapshot_equality() {
6329        let resp = mock_test_response(200, "hello", &[]);
6330        let snap1 = ResponseSnapshot::from_test_response(&resp);
6331        let snap2 = ResponseSnapshot::from_test_response(&resp);
6332        assert_eq!(snap1, snap2);
6333    }
6334
6335    #[test]
6336    fn snapshot_diff_status() {
6337        let s1 = ResponseSnapshot {
6338            status: 200,
6339            headers: vec![],
6340            body: "ok".to_string(),
6341            body_json: None,
6342        };
6343        let s2 = ResponseSnapshot {
6344            status: 404,
6345            ..s1.clone()
6346        };
6347        let diff = s1.diff(&s2);
6348        assert!(diff.contains("200 vs 404"));
6349    }
6350
6351    #[test]
6352    fn snapshot_diff_body() {
6353        let s1 = ResponseSnapshot {
6354            status: 200,
6355            headers: vec![],
6356            body: "hello".to_string(),
6357            body_json: None,
6358        };
6359        let s2 = ResponseSnapshot {
6360            body: "world".to_string(),
6361            ..s1.clone()
6362        };
6363        let diff = s1.diff(&s2);
6364        assert!(diff.contains("Body:"));
6365    }
6366
6367    #[test]
6368    fn snapshot_diff_no_differences() {
6369        let s = ResponseSnapshot {
6370            status: 200,
6371            headers: vec![],
6372            body: "ok".to_string(),
6373            body_json: None,
6374        };
6375        assert_eq!(s.diff(&s), "No differences");
6376    }
6377
6378    #[test]
6379    fn snapshot_mask_fields() {
6380        let resp = mock_test_response(
6381            200,
6382            r#"{"id":42,"name":"Alice","created_at":"2026-01-01"}"#,
6383            &[],
6384        );
6385        let snap = ResponseSnapshot::from_test_response(&resp)
6386            .mask_fields(&["id", "created_at"], "<MASKED>");
6387
6388        let json = snap.body_json.unwrap();
6389        assert_eq!(json["id"], "<MASKED>");
6390        assert_eq!(json["name"], "Alice");
6391        assert_eq!(json["created_at"], "<MASKED>");
6392    }
6393
6394    #[test]
6395    fn snapshot_mask_nested_fields() {
6396        let resp = mock_test_response(200, r#"{"user":{"id":1,"name":"Bob"}}"#, &[]);
6397        let snap =
6398            ResponseSnapshot::from_test_response(&resp).mask_fields(&["user.id"], "<MASKED>");
6399
6400        let json = snap.body_json.unwrap();
6401        assert_eq!(json["user"]["id"], "<MASKED>");
6402        assert_eq!(json["user"]["name"], "Bob");
6403    }
6404
6405    #[test]
6406    fn snapshot_save_and_load() {
6407        let snap = ResponseSnapshot {
6408            status: 200,
6409            headers: vec![("content-type".to_string(), "application/json".to_string())],
6410            body: r#"{"ok":true}"#.to_string(),
6411            body_json: Some(serde_json::json!({"ok": true})),
6412        };
6413
6414        let dir = std::env::temp_dir().join("fastapi_snapshot_test");
6415        let path = dir.join("test_snap.json");
6416        snap.save(&path).unwrap();
6417
6418        let loaded = ResponseSnapshot::load(&path).unwrap();
6419        assert_eq!(snap, loaded);
6420
6421        // Cleanup
6422        let _ = std::fs::remove_dir_all(&dir);
6423    }
6424
6425    #[test]
6426    fn snapshot_matches_ignoring_headers() {
6427        let s1 = ResponseSnapshot {
6428            status: 200,
6429            headers: vec![
6430                ("content-type".to_string(), "application/json".to_string()),
6431                ("x-request-id".to_string(), "abc".to_string()),
6432            ],
6433            body: "ok".to_string(),
6434            body_json: None,
6435        };
6436        let s2 = ResponseSnapshot {
6437            headers: vec![
6438                ("content-type".to_string(), "application/json".to_string()),
6439                ("x-request-id".to_string(), "xyz".to_string()),
6440            ],
6441            ..s1.clone()
6442        };
6443
6444        assert!(!s1.matches_ignoring_headers(&s2, &[]));
6445        assert!(s1.matches_ignoring_headers(&s2, &["X-Request-Id"]));
6446    }
6447
6448    #[test]
6449    fn snapshot_with_selected_headers() {
6450        let resp = mock_test_response(
6451            200,
6452            "ok",
6453            &[
6454                ("content-type", "text/plain"),
6455                ("x-request-id", "abc123"),
6456                ("x-trace-id", "trace-456"),
6457            ],
6458        );
6459        let snap = ResponseSnapshot::from_test_response_with_headers(&resp, &["content-type"]);
6460
6461        assert_eq!(snap.headers.len(), 1);
6462        assert_eq!(snap.headers[0].0, "content-type");
6463    }
6464
6465    #[test]
6466    fn snapshot_json_structural_comparison() {
6467        // Same JSON, different key order
6468        let s1 = ResponseSnapshot {
6469            status: 200,
6470            headers: vec![],
6471            body: r#"{"a":1,"b":2}"#.to_string(),
6472            body_json: Some(serde_json::json!({"a": 1, "b": 2})),
6473        };
6474        let s2 = ResponseSnapshot {
6475            body: r#"{"b":2,"a":1}"#.to_string(),
6476            body_json: Some(serde_json::json!({"b": 2, "a": 1})),
6477            ..s1.clone()
6478        };
6479
6480        // PartialEq compares body strings too, so they differ
6481        assert_ne!(s1, s2);
6482        // But matches_ignoring_headers uses JSON structural comparison
6483        assert!(s1.matches_ignoring_headers(&s2, &[]));
6484    }
6485}
6486
6487#[cfg(test)]
6488mod mock_server_tests {
6489    use super::*;
6490    use serial_test::serial;
6491
6492    #[test]
6493    #[serial(testing_network)]
6494    fn mock_server_starts_and_responds() {
6495        let server = MockServer::start();
6496        server.mock_response("/hello", MockResponse::ok().body_str("Hello, World!"));
6497
6498        // Make a simple HTTP request
6499        let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6500        stream
6501            .write_all(b"GET /hello HTTP/1.1\r\nHost: localhost\r\n\r\n")
6502            .unwrap();
6503
6504        let mut response = String::new();
6505        stream.read_to_string(&mut response).unwrap();
6506
6507        assert!(response.contains("200 OK"));
6508        assert!(response.contains("Hello, World!"));
6509    }
6510
6511    #[test]
6512    #[serial(testing_network)]
6513    fn mock_server_records_requests() {
6514        let server = MockServer::start();
6515
6516        // Make a request
6517        let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6518        stream
6519            .write_all(b"GET /api/users HTTP/1.1\r\nHost: localhost\r\nX-Custom: value\r\n\r\n")
6520            .unwrap();
6521        let mut response = Vec::new();
6522        let _ = stream.read_to_end(&mut response);
6523
6524        // Give the server time to process
6525        thread::sleep(Duration::from_millis(50));
6526
6527        let requests = server.requests();
6528        assert_eq!(requests.len(), 1);
6529        assert_eq!(requests[0].method, "GET");
6530        assert_eq!(requests[0].path, "/api/users");
6531        assert_eq!(requests[0].header("x-custom"), Some("value"));
6532    }
6533
6534    #[test]
6535    #[serial(testing_network)]
6536    fn mock_server_handles_post_with_body() {
6537        let server = MockServer::start();
6538        server.mock_response(
6539            "/api/create",
6540            MockResponse::with_status(201).body_str("Created"),
6541        );
6542
6543        let body = r#"{"name":"test"}"#;
6544        let request = format!(
6545            "POST /api/create HTTP/1.1\r\nHost: localhost\r\nContent-Length: {}\r\nContent-Type: application/json\r\n\r\n{}",
6546            body.len(),
6547            body
6548        );
6549
6550        let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6551        stream.write_all(request.as_bytes()).unwrap();
6552        let mut response = String::new();
6553        stream.read_to_string(&mut response).unwrap();
6554
6555        assert!(response.contains("201 Created"));
6556
6557        thread::sleep(Duration::from_millis(50));
6558        let requests = server.requests();
6559        assert_eq!(requests.len(), 1);
6560        assert_eq!(requests[0].method, "POST");
6561        assert_eq!(requests[0].body_text(), body);
6562    }
6563
6564    #[test]
6565    #[serial(testing_network)]
6566    fn mock_server_pattern_matching() {
6567        let server = MockServer::start();
6568        server.mock_response("/api/*", MockResponse::ok().body_str("API Response"));
6569
6570        let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6571        stream
6572            .write_all(b"GET /api/users/123 HTTP/1.1\r\nHost: localhost\r\n\r\n")
6573            .unwrap();
6574        let mut response = String::new();
6575        stream.read_to_string(&mut response).unwrap();
6576
6577        assert!(response.contains("API Response"));
6578    }
6579
6580    #[test]
6581    #[serial(testing_network)]
6582    fn mock_server_default_response() {
6583        let server = MockServer::start();
6584
6585        let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6586        stream
6587            .write_all(b"GET /unknown HTTP/1.1\r\nHost: localhost\r\n\r\n")
6588            .unwrap();
6589        let mut response = String::new();
6590        stream.read_to_string(&mut response).unwrap();
6591
6592        assert!(response.contains("404"));
6593    }
6594
6595    #[test]
6596    #[serial(testing_network)]
6597    fn mock_server_url_helpers() {
6598        let server = MockServer::start();
6599
6600        let url = server.url();
6601        assert!(url.starts_with("http://127.0.0.1:"));
6602
6603        let api_url = server.url_for("/api/users");
6604        assert!(api_url.contains("/api/users"));
6605    }
6606
6607    #[test]
6608    #[serial(testing_network)]
6609    fn mock_server_clear_requests() {
6610        let server = MockServer::start();
6611
6612        // Make a request
6613        let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6614        stream
6615            .write_all(b"GET /test HTTP/1.1\r\nHost: localhost\r\n\r\n")
6616            .unwrap();
6617        let mut response = Vec::new();
6618        let _ = stream.read_to_end(&mut response);
6619
6620        thread::sleep(Duration::from_millis(50));
6621        assert_eq!(server.request_count(), 1);
6622
6623        server.clear_requests();
6624        assert_eq!(server.request_count(), 0);
6625    }
6626
6627    #[test]
6628    #[serial(testing_network)]
6629    fn mock_server_wait_for_requests() {
6630        let server = MockServer::start();
6631
6632        // Spawn a thread that will make a request after a delay
6633        let addr = server.addr();
6634        thread::spawn(move || {
6635            thread::sleep(Duration::from_millis(50));
6636            let mut stream = StdTcpStream::connect(addr).expect("Failed to connect");
6637            stream
6638                .write_all(b"GET /delayed HTTP/1.1\r\nHost: localhost\r\n\r\n")
6639                .unwrap();
6640        });
6641
6642        let received = server.wait_for_requests(1, Duration::from_millis(500));
6643        assert!(received);
6644        assert_eq!(server.request_count(), 1);
6645    }
6646
6647    #[test]
6648    #[serial(testing_network)]
6649    fn mock_server_assert_helpers() {
6650        let server = MockServer::start();
6651
6652        let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6653        stream
6654            .write_all(b"GET /expected HTTP/1.1\r\nHost: localhost\r\n\r\n")
6655            .unwrap();
6656        let mut response = Vec::new();
6657        let _ = stream.read_to_end(&mut response);
6658
6659        thread::sleep(Duration::from_millis(50));
6660
6661        server.assert_received("/expected");
6662        server.assert_not_received("/not-expected");
6663        server.assert_request_count(1);
6664    }
6665
6666    #[test]
6667    #[serial(testing_network)]
6668    fn mock_server_query_string_parsing() {
6669        let server = MockServer::start();
6670
6671        let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6672        stream
6673            .write_all(b"GET /search?q=rust&limit=10 HTTP/1.1\r\nHost: localhost\r\n\r\n")
6674            .unwrap();
6675        let mut response = Vec::new();
6676        let _ = stream.read_to_end(&mut response);
6677
6678        thread::sleep(Duration::from_millis(50));
6679
6680        let requests = server.requests();
6681        assert_eq!(requests.len(), 1);
6682        assert_eq!(requests[0].path, "/search");
6683        assert_eq!(requests[0].query, Some("q=rust&limit=10".to_string()));
6684        assert_eq!(requests[0].url(), "/search?q=rust&limit=10");
6685    }
6686
6687    #[test]
6688    fn mock_response_json() {
6689        #[derive(serde::Serialize)]
6690        struct User {
6691            name: String,
6692        }
6693
6694        let response = MockResponse::ok().json(&User {
6695            name: "Alice".to_string(),
6696        });
6697        let bytes = response.to_http_response();
6698        let http = String::from_utf8_lossy(&bytes);
6699
6700        assert!(http.contains("application/json"));
6701        assert!(http.contains("Alice"));
6702    }
6703
6704    #[test]
6705    fn recorded_request_helpers() {
6706        let request = RecordedRequest {
6707            method: "GET".to_string(),
6708            path: "/api/users".to_string(),
6709            query: Some("page=1".to_string()),
6710            headers: vec![("Content-Type".to_string(), "application/json".to_string())],
6711            body: b"test body".to_vec(),
6712            timestamp: std::time::Instant::now(),
6713        };
6714
6715        assert_eq!(request.body_text(), "test body");
6716        assert_eq!(request.header("content-type"), Some("application/json"));
6717        assert_eq!(request.url(), "/api/users?page=1");
6718    }
6719}
6720
6721#[cfg(test)]
6722mod e2e_tests {
6723    use super::*;
6724
6725    // Create a simple test handler for E2E testing
6726    fn test_handler(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
6727        let path = req.path();
6728        let response = match path {
6729            "/" => Response::ok().body(ResponseBody::Bytes(b"Home".to_vec())),
6730            "/login" => Response::ok().body(ResponseBody::Bytes(b"Login Page".to_vec())),
6731            "/dashboard" => Response::ok().body(ResponseBody::Bytes(b"Dashboard".to_vec())),
6732            "/api/users" => {
6733                Response::ok().body(ResponseBody::Bytes(b"[\"Alice\",\"Bob\"]".to_vec()))
6734            }
6735            "/fail" => Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
6736                .body(ResponseBody::Bytes(b"Error".to_vec())),
6737            _ => Response::with_status(StatusCode::NOT_FOUND)
6738                .body(ResponseBody::Bytes(b"Not Found".to_vec())),
6739        };
6740        std::future::ready(response)
6741    }
6742
6743    #[test]
6744    fn e2e_scenario_all_steps_pass() {
6745        let client = TestClient::new(test_handler);
6746        let mut scenario = E2EScenario::new("Basic Navigation", client);
6747
6748        scenario.step("Visit home page", |client| {
6749            let response = client.get("/").send();
6750            assert_eq!(response.status().as_u16(), 200);
6751            assert_eq!(response.text(), "Home");
6752        });
6753
6754        scenario.step("Visit login page", |client| {
6755            let response = client.get("/login").send();
6756            assert_eq!(response.status().as_u16(), 200);
6757        });
6758
6759        assert!(scenario.passed());
6760        assert_eq!(scenario.steps().len(), 2);
6761        assert!(scenario.steps().iter().all(|s| s.result.is_passed()));
6762    }
6763
6764    #[test]
6765    fn e2e_scenario_step_failure() {
6766        let client = TestClient::new(test_handler);
6767        let mut scenario = E2EScenario::new("Failure Test", client).stop_on_failure(true);
6768
6769        scenario.step("First step passes", |client| {
6770            let response = client.get("/").send();
6771            assert_eq!(response.status().as_u16(), 200);
6772        });
6773
6774        scenario.step("Second step fails", |_client| {
6775            panic!("Intentional failure");
6776        });
6777
6778        scenario.step("Third step skipped", |client| {
6779            let response = client.get("/dashboard").send();
6780            assert_eq!(response.status().as_u16(), 200);
6781        });
6782
6783        assert!(!scenario.passed());
6784        assert_eq!(scenario.steps().len(), 3);
6785        assert!(scenario.steps()[0].result.is_passed());
6786        assert!(scenario.steps()[1].result.is_failed());
6787        assert!(matches!(scenario.steps()[2].result, E2EStepResult::Skipped));
6788    }
6789
6790    #[test]
6791    fn e2e_scenario_continue_on_failure() {
6792        let client = TestClient::new(test_handler);
6793        let mut scenario = E2EScenario::new("Continue Test", client).stop_on_failure(false);
6794
6795        scenario.step("First step fails", |_client| {
6796            panic!("First failure");
6797        });
6798
6799        scenario.step("Second step still runs", |client| {
6800            let response = client.get("/").send();
6801            assert_eq!(response.status().as_u16(), 200);
6802        });
6803
6804        assert!(!scenario.passed());
6805        assert_eq!(scenario.steps().len(), 2);
6806        assert!(scenario.steps()[0].result.is_failed());
6807        // Second step ran (not skipped) and passed
6808        assert!(scenario.steps()[1].result.is_passed());
6809    }
6810
6811    #[test]
6812    fn e2e_report_text_format() {
6813        let client = TestClient::new(test_handler);
6814        let mut scenario =
6815            E2EScenario::new("Report Test", client).description("Tests report generation");
6816
6817        scenario.step("Step 1", |client| {
6818            let _ = client.get("/").send();
6819        });
6820
6821        let report = scenario.report();
6822        let text = report.to_text();
6823
6824        assert!(text.contains("E2E Test Report: Report Test"));
6825        assert!(text.contains("Tests report generation"));
6826        assert!(text.contains("1 passed"));
6827        assert!(text.contains("Step 1"));
6828    }
6829
6830    #[test]
6831    fn e2e_report_json_format() {
6832        let client = TestClient::new(test_handler);
6833        let mut scenario = E2EScenario::new("JSON Test", client);
6834
6835        scenario.step("API call", |client| {
6836            let response = client.get("/api/users").send();
6837            assert_eq!(response.status().as_u16(), 200);
6838        });
6839
6840        let report = scenario.report();
6841        let json = report.to_json();
6842
6843        assert!(json.contains(r#""scenario": "JSON Test""#));
6844        assert!(json.contains(r#""passed": 1"#));
6845        assert!(json.contains(r#""name": "API call""#));
6846        assert!(json.contains(r#""status": "passed""#));
6847    }
6848
6849    #[test]
6850    fn e2e_report_html_format() {
6851        let client = TestClient::new(test_handler);
6852        let mut scenario = E2EScenario::new("HTML Test", client);
6853
6854        scenario.step("Web visit", |client| {
6855            let _ = client.get("/").send();
6856        });
6857
6858        let report = scenario.report();
6859        let html = report.to_html();
6860
6861        assert!(html.contains("<!DOCTYPE html>"));
6862        assert!(html.contains("E2E Report: HTML Test"));
6863        assert!(html.contains("1 passed"));
6864        assert!(html.contains("Web visit"));
6865    }
6866
6867    #[test]
6868    fn e2e_step_timing() {
6869        let client = TestClient::new(test_handler);
6870        let mut scenario = E2EScenario::new("Timing Test", client);
6871
6872        scenario.step("Timed step", |_client| {
6873            // Small delay to ensure measurable duration
6874            std::thread::sleep(std::time::Duration::from_millis(10));
6875        });
6876
6877        assert!(scenario.steps()[0].duration >= std::time::Duration::from_millis(10));
6878    }
6879
6880    #[test]
6881    fn e2e_logs_captured() {
6882        let client = TestClient::new(test_handler);
6883        let mut scenario = E2EScenario::new("Log Test", client);
6884
6885        scenario.log("Manual log entry");
6886        scenario.step("Logged step", |_client| {});
6887
6888        assert!(
6889            scenario
6890                .logs()
6891                .iter()
6892                .any(|l| l.contains("Manual log entry"))
6893        );
6894        assert!(
6895            scenario
6896                .logs()
6897                .iter()
6898                .any(|l| l.contains("[START] Logged step"))
6899        );
6900        assert!(
6901            scenario
6902                .logs()
6903                .iter()
6904                .any(|l| l.contains("[PASS] Logged step"))
6905        );
6906    }
6907
6908    #[test]
6909    fn e2e_try_step_with_result() {
6910        let client = TestClient::new(test_handler);
6911        let mut scenario = E2EScenario::new("Try Step Test", client);
6912
6913        let result: Result<(), &str> = scenario.try_step("Success step", |client| {
6914            let response = client.get("/").send();
6915            if response.status().as_u16() == 200 {
6916                Ok(())
6917            } else {
6918                Err("Unexpected status")
6919            }
6920        });
6921
6922        assert!(result.is_ok());
6923        assert!(scenario.passed());
6924    }
6925
6926    #[test]
6927    fn e2e_escape_functions() {
6928        // Test JSON escaping
6929        assert_eq!(escape_json("hello"), "hello");
6930        assert_eq!(escape_json("a\"b"), "a\\\"b");
6931        assert_eq!(escape_json("a\nb"), "a\\nb");
6932
6933        // Test HTML escaping
6934        assert_eq!(escape_html("hello"), "hello");
6935        assert_eq!(escape_html("<script>"), "&lt;script&gt;");
6936        assert_eq!(escape_html("a&b"), "a&amp;b");
6937    }
6938
6939    #[test]
6940    fn e2e_step_result_helpers() {
6941        let passed = E2EStepResult::Passed;
6942        let failed = E2EStepResult::Failed("error".to_string());
6943        let skipped = E2EStepResult::Skipped;
6944
6945        assert!(passed.is_passed());
6946        assert!(!passed.is_failed());
6947
6948        assert!(!failed.is_passed());
6949        assert!(failed.is_failed());
6950
6951        assert!(!skipped.is_passed());
6952        assert!(!skipped.is_failed());
6953    }
6954}
6955
6956// =============================================================================
6957// Integration Test Framework
6958// =============================================================================
6959
6960/// Trait for test fixtures that set up and tear down test data.
6961///
6962/// Implement this trait for resources that need initialization before tests
6963/// and cleanup afterwards (databases, temp files, mock services, etc.).
6964///
6965/// # Example
6966///
6967/// ```ignore
6968/// use fastapi_core::testing::TestFixture;
6969///
6970/// struct DatabaseFixture {
6971///     conn: DatabaseConnection,
6972///     users_created: Vec<i64>,
6973/// }
6974///
6975/// impl TestFixture for DatabaseFixture {
6976///     fn setup() -> Self {
6977///         let conn = DatabaseConnection::test();
6978///         DatabaseFixture { conn, users_created: vec![] }
6979///     }
6980///
6981///     fn teardown(&mut self) {
6982///         // Delete any users we created
6983///         for id in &self.users_created {
6984///             self.conn.delete_user(*id);
6985///         }
6986///     }
6987/// }
6988/// ```
6989pub trait TestFixture: Sized + Send {
6990    /// Set up the fixture before the test.
6991    fn setup() -> Self;
6992
6993    /// Tear down the fixture after the test.
6994    ///
6995    /// This is called even if the test panics, ensuring cleanup happens.
6996    fn teardown(&mut self) {}
6997}
6998
6999/// A guard that automatically calls teardown when dropped.
7000///
7001/// This ensures fixtures are cleaned up even if the test panics.
7002pub struct FixtureGuard<F: TestFixture> {
7003    fixture: Option<F>,
7004}
7005
7006impl<F: TestFixture> FixtureGuard<F> {
7007    /// Creates a new fixture guard, setting up the fixture.
7008    pub fn new() -> Self {
7009        Self {
7010            fixture: Some(F::setup()),
7011        }
7012    }
7013
7014    /// Get a reference to the fixture.
7015    pub fn get(&self) -> &F {
7016        self.fixture.as_ref().unwrap()
7017    }
7018
7019    /// Get a mutable reference to the fixture.
7020    pub fn get_mut(&mut self) -> &mut F {
7021        self.fixture.as_mut().unwrap()
7022    }
7023}
7024
7025impl<F: TestFixture> Default for FixtureGuard<F> {
7026    fn default() -> Self {
7027        Self::new()
7028    }
7029}
7030
7031impl<F: TestFixture> Drop for FixtureGuard<F> {
7032    fn drop(&mut self) {
7033        if let Some(mut fixture) = self.fixture.take() {
7034            fixture.teardown();
7035        }
7036    }
7037}
7038
7039impl<F: TestFixture> std::ops::Deref for FixtureGuard<F> {
7040    type Target = F;
7041
7042    fn deref(&self) -> &Self::Target {
7043        self.get()
7044    }
7045}
7046
7047impl<F: TestFixture> std::ops::DerefMut for FixtureGuard<F> {
7048    fn deref_mut(&mut self) -> &mut Self::Target {
7049        self.get_mut()
7050    }
7051}
7052
7053/// Context for integration tests that manages fixtures and test client.
7054///
7055/// Provides a structured way to run multi-step integration tests with
7056/// automatic fixture management and test isolation.
7057///
7058/// # Example
7059///
7060/// ```ignore
7061/// use fastapi_core::testing::{IntegrationTest, TestFixture};
7062/// use std::sync::Arc;
7063///
7064/// // Define a fixture (e.g., for database state)
7065/// struct TestData {
7066///     user_id: i64,
7067/// }
7068///
7069/// impl TestFixture for TestData {
7070///     fn setup() -> Self {
7071///         // Create test data
7072///         TestData { user_id: 1 }
7073///     }
7074///
7075///     fn teardown(&mut self) {
7076///         // Clean up test data
7077///     }
7078/// }
7079///
7080/// #[test]
7081/// fn test_user_api() {
7082///     let app = Arc::new(App::builder()
7083///         .route("/users/{id}", Method::Get, get_user)
7084///         .build());
7085///
7086///     IntegrationTest::new("User API Test", app)
7087///         .with_fixture::<TestData>()
7088///         .run(|ctx| {
7089///             // Access fixture
7090///             let data = ctx.fixture::<TestData>().unwrap();
7091///
7092///             // Make requests through the full app stack
7093///             let response = ctx.get(&format!("/users/{}", data.user_id)).send();
7094///             assert_eq!(response.status().as_u16(), 200);
7095///         });
7096/// }
7097/// ```
7098pub struct IntegrationTest<H: Handler + 'static> {
7099    /// Test name.
7100    name: String,
7101    /// Test client wrapping the app.
7102    client: TestClient<H>,
7103    /// Registered fixtures (type-erased).
7104    fixtures: HashMap<std::any::TypeId, Box<dyn std::any::Any + Send>>,
7105    /// State reset hooks to run between tests.
7106    reset_hooks: Vec<Box<dyn Fn() + Send + Sync>>,
7107}
7108
7109impl<H: Handler + 'static> IntegrationTest<H> {
7110    /// Creates a new integration test context.
7111    pub fn new(name: impl Into<String>, handler: H) -> Self {
7112        Self {
7113            name: name.into(),
7114            client: TestClient::new(handler),
7115            fixtures: HashMap::new(),
7116            reset_hooks: Vec::new(),
7117        }
7118    }
7119
7120    /// Creates a new integration test with a specific seed for determinism.
7121    pub fn with_seed(name: impl Into<String>, handler: H, seed: u64) -> Self {
7122        Self {
7123            name: name.into(),
7124            client: TestClient::with_seed(handler, seed),
7125            fixtures: HashMap::new(),
7126            reset_hooks: Vec::new(),
7127        }
7128    }
7129
7130    /// Registers a fixture type for this test.
7131    ///
7132    /// The fixture will be set up before the test runs and torn down after.
7133    #[must_use]
7134    pub fn with_fixture<F: TestFixture + 'static>(mut self) -> Self {
7135        let guard = FixtureGuard::<F>::new();
7136        self.fixtures
7137            .insert(std::any::TypeId::of::<F>(), Box::new(guard));
7138        self
7139    }
7140
7141    /// Registers a state reset hook to run after the test.
7142    ///
7143    /// Useful for clearing caches, resetting global state, etc.
7144    #[must_use]
7145    pub fn on_reset<F: Fn() + Send + Sync + 'static>(mut self, f: F) -> Self {
7146        self.reset_hooks.push(Box::new(f));
7147        self
7148    }
7149
7150    /// Runs the integration test.
7151    ///
7152    /// The test function receives an `IntegrationTestContext` that provides
7153    /// access to the test client and fixtures.
7154    pub fn run<F>(mut self, test_fn: F)
7155    where
7156        F: FnOnce(&IntegrationTestContext<'_, H>) + std::panic::UnwindSafe,
7157    {
7158        // Create context
7159        let ctx = IntegrationTestContext {
7160            name: &self.name,
7161            client: &self.client,
7162            fixtures: &self.fixtures,
7163        };
7164
7165        // Wrap context for panic safety
7166        let ctx_ref = std::panic::AssertUnwindSafe(&ctx);
7167
7168        // Run test and capture result
7169        let result = std::panic::catch_unwind(|| {
7170            test_fn(&ctx_ref);
7171        });
7172
7173        // Run reset hooks regardless of outcome
7174        for hook in &self.reset_hooks {
7175            hook();
7176        }
7177
7178        // Clear cookies and dependency overrides
7179        self.client.clear_cookies();
7180        self.client.clear_dependency_overrides();
7181
7182        // Drop fixtures in reverse order (triggers teardown)
7183        self.fixtures.clear();
7184
7185        // Re-panic if test failed
7186        if let Err(e) = result {
7187            std::panic::resume_unwind(e);
7188        }
7189    }
7190}
7191
7192/// Context available during an integration test.
7193pub struct IntegrationTestContext<'a, H: Handler> {
7194    /// Test name.
7195    name: &'a str,
7196    /// Test client.
7197    client: &'a TestClient<H>,
7198    /// Registered fixtures.
7199    fixtures: &'a HashMap<std::any::TypeId, Box<dyn std::any::Any + Send>>,
7200}
7201
7202impl<'a, H: Handler + 'static> IntegrationTestContext<'a, H> {
7203    /// Returns the test name.
7204    #[must_use]
7205    pub fn name(&self) -> &str {
7206        self.name
7207    }
7208
7209    /// Returns the test client.
7210    #[must_use]
7211    pub fn client(&self) -> &TestClient<H> {
7212        self.client
7213    }
7214
7215    /// Gets a reference to a registered fixture.
7216    ///
7217    /// Returns `None` if the fixture type was not registered.
7218    #[must_use]
7219    pub fn fixture<F: TestFixture + 'static>(&self) -> Option<&F> {
7220        self.fixtures
7221            .get(&std::any::TypeId::of::<F>())
7222            .and_then(|boxed| boxed.downcast_ref::<FixtureGuard<F>>())
7223            .map(FixtureGuard::get)
7224    }
7225
7226    /// Gets a mutable reference to a registered fixture.
7227    ///
7228    /// Returns `None` if the fixture type was not registered.
7229    #[must_use]
7230    pub fn fixture_mut<F: TestFixture + 'static>(&self) -> Option<&mut F> {
7231        // This is safe because we only expose mutable access to the fixture content,
7232        // not to the guard itself. The borrow checker ensures single-threaded access.
7233        // Note: This requires interior mutability in the fixture or careful usage.
7234        None // Conservative: don't allow mutable access through shared ref
7235    }
7236
7237    // Delegate HTTP methods to client for convenience
7238
7239    /// Starts building a GET request.
7240    pub fn get(&self, path: &str) -> RequestBuilder<'_, H> {
7241        self.client.get(path)
7242    }
7243
7244    /// Starts building a POST request.
7245    pub fn post(&self, path: &str) -> RequestBuilder<'_, H> {
7246        self.client.post(path)
7247    }
7248
7249    /// Starts building a PUT request.
7250    pub fn put(&self, path: &str) -> RequestBuilder<'_, H> {
7251        self.client.put(path)
7252    }
7253
7254    /// Starts building a DELETE request.
7255    pub fn delete(&self, path: &str) -> RequestBuilder<'_, H> {
7256        self.client.delete(path)
7257    }
7258
7259    /// Starts building a PATCH request.
7260    pub fn patch(&self, path: &str) -> RequestBuilder<'_, H> {
7261        self.client.patch(path)
7262    }
7263
7264    /// Starts building an OPTIONS request.
7265    pub fn options(&self, path: &str) -> RequestBuilder<'_, H> {
7266        self.client.options(path)
7267    }
7268
7269    /// Starts building a request with a custom method.
7270    pub fn request(&self, method: Method, path: &str) -> RequestBuilder<'_, H> {
7271        self.client.request(method, path)
7272    }
7273}
7274
7275// =============================================================================
7276// TestServer Unit Tests
7277// =============================================================================
7278
7279#[cfg(test)]
7280mod test_server_tests {
7281    use super::*;
7282    use crate::app::App;
7283    use serial_test::serial;
7284    use std::net::TcpStream as StdTcpStreamAlias;
7285
7286    fn make_test_app() -> App {
7287        App::builder()
7288            .get("/health", |_ctx: &RequestContext, _req: &mut Request| {
7289                std::future::ready(
7290                    Response::ok()
7291                        .header("content-type", b"text/plain".to_vec())
7292                        .body(ResponseBody::Bytes(b"OK".to_vec())),
7293                )
7294            })
7295            .get("/hello", |_ctx: &RequestContext, _req: &mut Request| {
7296                std::future::ready(
7297                    Response::ok()
7298                        .header("content-type", b"application/json".to_vec())
7299                        .body(ResponseBody::Bytes(
7300                            br#"{"message":"Hello, World!"}"#.to_vec(),
7301                        )),
7302                )
7303            })
7304            .post("/echo", |_ctx: &RequestContext, req: &mut Request| {
7305                let body = match req.body() {
7306                    Body::Bytes(b) => b.clone(),
7307                    _ => Vec::new(),
7308                };
7309                std::future::ready(
7310                    Response::ok()
7311                        .header("content-type", b"application/octet-stream".to_vec())
7312                        .body(ResponseBody::Bytes(body)),
7313                )
7314            })
7315            .build()
7316    }
7317
7318    fn send_request(addr: SocketAddr, request: &[u8]) -> String {
7319        let mut stream = StdTcpStreamAlias::connect(addr).expect("Failed to connect to TestServer");
7320        stream
7321            .set_read_timeout(Some(Duration::from_secs(10)))
7322            .expect("set_read_timeout");
7323        stream.write_all(request).expect("Failed to write request");
7324        stream.flush().expect("Failed to flush");
7325
7326        // Drain to EOF: the server sends FIN after writing the full response
7327        // (see `graceful_close`), so reading until 0 reliably collects the
7328        // payload on every supported platform without relying on a single
7329        // `read` call that may return only the headers.
7330        let mut out = Vec::with_capacity(4096);
7331        let mut buf = [0u8; 8192];
7332        loop {
7333            match stream.read(&mut buf) {
7334                Ok(0) => break,
7335                Ok(n) => out.extend_from_slice(&buf[..n]),
7336                Err(err) if err.kind() == std::io::ErrorKind::ConnectionReset => break,
7337                Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => break,
7338                Err(err) => panic!("Failed to read response: {err:?}"),
7339            }
7340        }
7341        String::from_utf8_lossy(&out).to_string()
7342    }
7343
7344    #[test]
7345    #[serial(testing_network)]
7346    fn test_server_starts_and_responds() {
7347        let app = make_test_app();
7348        let server = TestServer::start(app);
7349
7350        let response = send_request(
7351            server.addr(),
7352            b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7353        );
7354
7355        assert!(
7356            response.contains("200 OK"),
7357            "Expected 200 OK, got: {response}"
7358        );
7359        assert!(response.contains("OK"), "Expected body 'OK'");
7360    }
7361
7362    #[test]
7363    #[serial(testing_network)]
7364    fn test_server_json_response() {
7365        let app = make_test_app();
7366        let server = TestServer::start(app);
7367
7368        let response = send_request(
7369            server.addr(),
7370            b"GET /hello HTTP/1.1\r\nHost: localhost\r\n\r\n",
7371        );
7372
7373        assert!(response.contains("200 OK"));
7374        assert!(response.contains("application/json"));
7375        assert!(response.contains(r#"{"message":"Hello, World!"}"#));
7376    }
7377
7378    #[test]
7379    #[serial(testing_network)]
7380    fn test_server_post_with_body() {
7381        let app = make_test_app();
7382        let server = TestServer::start(app);
7383
7384        let request =
7385            b"POST /echo HTTP/1.1\r\nHost: localhost\r\nContent-Length: 11\r\n\r\nHello World";
7386        let response = send_request(server.addr(), request);
7387
7388        assert!(response.contains("200 OK"));
7389        assert!(response.contains("Hello World"));
7390    }
7391
7392    #[test]
7393    #[serial(testing_network)]
7394    fn test_server_logs_requests() {
7395        let app = make_test_app();
7396        let server = TestServer::start(app);
7397
7398        // Make a request
7399        send_request(
7400            server.addr(),
7401            b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7402        );
7403
7404        let logs = server.log_entries();
7405        assert_eq!(logs.len(), 1);
7406        assert_eq!(logs[0].method, "GET");
7407        assert_eq!(logs[0].path, "/health");
7408        assert_eq!(logs[0].status, 200);
7409    }
7410
7411    #[test]
7412    #[serial(testing_network)]
7413    fn test_server_request_count() {
7414        let app = make_test_app();
7415        let server = TestServer::start(app);
7416
7417        assert_eq!(server.request_count(), 0);
7418
7419        send_request(
7420            server.addr(),
7421            b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7422        );
7423        send_request(
7424            server.addr(),
7425            b"GET /hello HTTP/1.1\r\nHost: localhost\r\n\r\n",
7426        );
7427
7428        assert_eq!(server.request_count(), 2);
7429    }
7430
7431    #[test]
7432    #[serial(testing_network)]
7433    fn test_server_clear_logs() {
7434        let app = make_test_app();
7435        let server = TestServer::start(app);
7436
7437        send_request(
7438            server.addr(),
7439            b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7440        );
7441        assert_eq!(server.request_count(), 1);
7442
7443        server.clear_logs();
7444        assert_eq!(server.request_count(), 0);
7445    }
7446
7447    #[test]
7448    #[serial(testing_network)]
7449    fn test_server_url_helpers() {
7450        let app = make_test_app();
7451        let server = TestServer::start(app);
7452
7453        assert!(server.url().starts_with("http://127.0.0.1:"));
7454        assert!(server.url_for("/health").ends_with("/health"));
7455        assert!(server.url_for("health").ends_with("/health"));
7456        assert!(server.port() > 0);
7457    }
7458
7459    #[test]
7460    #[serial(testing_network)]
7461    fn test_server_shutdown() {
7462        let app = make_test_app();
7463        let server = TestServer::start(app);
7464        let addr = server.addr();
7465
7466        // Server should respond before shutdown
7467        let response = send_request(addr, b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n");
7468        assert!(response.contains("200 OK"));
7469
7470        // Signal shutdown
7471        server.shutdown();
7472        assert!(server.is_shutdown());
7473    }
7474
7475    #[test]
7476    #[serial(testing_network)]
7477    fn test_server_config_no_logging() {
7478        let app = make_test_app();
7479        let config = TestServerConfig::new().log_requests(false);
7480        let server = TestServer::start_with_config(app, config);
7481
7482        send_request(
7483            server.addr(),
7484            b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7485        );
7486
7487        // With logging disabled, no log entries should be recorded
7488        assert_eq!(server.request_count(), 0);
7489    }
7490
7491    #[test]
7492    #[serial(testing_network)]
7493    fn test_server_bad_request() {
7494        let app = make_test_app();
7495        let server = TestServer::start(app);
7496
7497        // Send garbage data
7498        let response = send_request(server.addr(), b"NOT_HTTP_AT_ALL");
7499
7500        assert!(response.contains("400 Bad Request"));
7501    }
7502
7503    #[test]
7504    #[serial(testing_network)]
7505    fn test_server_content_length_header() {
7506        let app = make_test_app();
7507        let server = TestServer::start(app);
7508
7509        let response = send_request(
7510            server.addr(),
7511            b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7512        );
7513
7514        // Response should include content-length
7515        assert!(
7516            response.contains("content-length: 2"),
7517            "Expected content-length: 2, got: {response}"
7518        );
7519    }
7520
7521    #[test]
7522    #[serial(testing_network)]
7523    fn test_server_multiple_requests_sequential() {
7524        let app = make_test_app();
7525        let server = TestServer::start(app);
7526
7527        for _ in 0..5 {
7528            let response = send_request(
7529                server.addr(),
7530                b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7531            );
7532            assert!(response.contains("200 OK"));
7533        }
7534
7535        assert_eq!(server.request_count(), 5);
7536    }
7537
7538    #[test]
7539    #[serial(testing_network)]
7540    fn test_server_log_entry_has_timing() {
7541        let app = make_test_app();
7542        let server = TestServer::start(app);
7543
7544        send_request(
7545            server.addr(),
7546            b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7547        );
7548
7549        let logs = server.log_entries();
7550        assert_eq!(logs.len(), 1);
7551        // Duration should be non-zero but reasonable (under 1 second)
7552        assert!(logs[0].duration < Duration::from_secs(1));
7553    }
7554
7555    // =========================================================================
7556    // Graceful Shutdown E2E Tests (bd-14if)
7557    // =========================================================================
7558
7559    #[test]
7560    #[serial(testing_network)]
7561    fn test_server_shutdown_controller_available() {
7562        let app = make_test_app();
7563        let server = TestServer::start(app);
7564
7565        // ShutdownController should be accessible
7566        let controller = server.shutdown_controller();
7567        assert!(!controller.is_shutting_down());
7568        assert_eq!(controller.phase(), crate::shutdown::ShutdownPhase::Running);
7569    }
7570
7571    #[test]
7572    #[serial(testing_network)]
7573    fn test_server_shutdown_triggers_controller() {
7574        let app = make_test_app();
7575        let server = TestServer::start(app);
7576
7577        // Server should be running normally
7578        assert!(!server.shutdown_controller().is_shutting_down());
7579
7580        // Trigger graceful shutdown
7581        server.shutdown();
7582
7583        // Both the server flag and controller should reflect shutdown
7584        assert!(server.is_shutdown());
7585        assert!(server.shutdown_controller().is_shutting_down());
7586        assert_eq!(
7587            server.shutdown_controller().phase(),
7588            crate::shutdown::ShutdownPhase::StopAccepting
7589        );
7590    }
7591
7592    #[test]
7593    #[serial(testing_network)]
7594    fn test_server_requests_complete_before_shutdown() {
7595        let app = make_test_app();
7596        let server = TestServer::start(app);
7597
7598        // Make a normal request before shutdown
7599        let response = send_request(
7600            server.addr(),
7601            b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7602        );
7603        assert!(response.contains("200 OK"));
7604        assert_eq!(server.request_count(), 1);
7605
7606        // Signal shutdown
7607        server.shutdown();
7608
7609        // Verify the request completed and was logged
7610        let logs = server.log_entries();
7611        assert_eq!(logs.len(), 1);
7612        assert_eq!(logs[0].status, 200);
7613        assert_eq!(logs[0].path, "/health");
7614    }
7615
7616    #[test]
7617    #[serial(testing_network)]
7618    fn test_server_in_flight_tracking() {
7619        let app = make_test_app();
7620        let server = TestServer::start(app);
7621
7622        // Initially no in-flight requests
7623        assert_eq!(server.in_flight_count(), 0);
7624
7625        // The in-flight guard is managed internally by the server loop,
7626        // so after request completion it should return to 0
7627        send_request(
7628            server.addr(),
7629            b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7630        );
7631
7632        // Wait for the in-flight count to return to 0 (bd-2emz fix)
7633        // There's a small race between client receiving response and
7634        // server dropping the InFlightGuard, so we spin briefly.
7635        let start = std::time::Instant::now();
7636        let timeout = std::time::Duration::from_millis(500);
7637        while server.in_flight_count() > 0 && start.elapsed() < timeout {
7638            std::thread::sleep(std::time::Duration::from_millis(1));
7639        }
7640        assert_eq!(
7641            server.in_flight_count(),
7642            0,
7643            "In-flight count should return to 0 after request completes"
7644        );
7645    }
7646
7647    #[test]
7648    #[serial(testing_network)]
7649    fn test_server_in_flight_guard_tracks_correctly() {
7650        let app = make_test_app();
7651        let server = TestServer::start(app);
7652
7653        // Manually track requests via the controller
7654        let controller = server.shutdown_controller();
7655        assert_eq!(controller.in_flight_count(), 0);
7656
7657        let guard1 = controller.track_request();
7658        assert_eq!(controller.in_flight_count(), 1);
7659
7660        let guard2 = controller.track_request();
7661        assert_eq!(controller.in_flight_count(), 2);
7662
7663        drop(guard1);
7664        assert_eq!(controller.in_flight_count(), 1);
7665
7666        drop(guard2);
7667        assert_eq!(controller.in_flight_count(), 0);
7668    }
7669
7670    #[test]
7671    #[serial(testing_network)]
7672    fn test_server_shutdown_hooks_executed() {
7673        let app = make_test_app();
7674        let server = TestServer::start(app);
7675
7676        // Register shutdown hooks
7677        let hook_executed = Arc::new(AtomicBool::new(false));
7678        let hook_executed_clone = Arc::clone(&hook_executed);
7679        server.shutdown_controller().register_hook(move || {
7680            hook_executed_clone.store(true, std::sync::atomic::Ordering::Release);
7681        });
7682
7683        assert!(!hook_executed.load(std::sync::atomic::Ordering::Acquire));
7684
7685        // Trigger shutdown — hooks run in the server loop when it exits
7686        server.shutdown();
7687
7688        // Wait for background thread to finish
7689        // Drop the server to join the thread
7690        drop(server);
7691
7692        assert!(
7693            hook_executed.load(std::sync::atomic::Ordering::Acquire),
7694            "Shutdown hook should have been executed"
7695        );
7696    }
7697
7698    #[test]
7699    #[serial(testing_network)]
7700    fn test_server_multiple_shutdown_hooks_lifo() {
7701        let app = make_test_app();
7702        let server = TestServer::start(app);
7703
7704        let execution_order = Arc::new(Mutex::new(Vec::new()));
7705
7706        let order1 = Arc::clone(&execution_order);
7707        server.shutdown_controller().register_hook(move || {
7708            order1.lock().push(1);
7709        });
7710
7711        let order2 = Arc::clone(&execution_order);
7712        server.shutdown_controller().register_hook(move || {
7713            order2.lock().push(2);
7714        });
7715
7716        let order3 = Arc::clone(&execution_order);
7717        server.shutdown_controller().register_hook(move || {
7718            order3.lock().push(3);
7719        });
7720
7721        // Trigger shutdown and wait for thread to finish
7722        server.shutdown();
7723        drop(server);
7724
7725        // Hooks should run in LIFO order (3, 2, 1)
7726        let order = execution_order.lock();
7727        assert_eq!(*order, vec![3, 2, 1]);
7728    }
7729
7730    #[test]
7731    #[serial(testing_network)]
7732    fn test_server_shutdown_controller_phase_progression() {
7733        let app = make_test_app();
7734        let server = TestServer::start(app);
7735
7736        let controller = server.shutdown_controller();
7737        assert_eq!(controller.phase(), crate::shutdown::ShutdownPhase::Running);
7738
7739        // Advance through phases manually
7740        assert!(controller.advance_phase());
7741        assert_eq!(
7742            controller.phase(),
7743            crate::shutdown::ShutdownPhase::StopAccepting
7744        );
7745
7746        assert!(controller.advance_phase());
7747        assert_eq!(
7748            controller.phase(),
7749            crate::shutdown::ShutdownPhase::ShutdownFlagged
7750        );
7751
7752        assert!(controller.advance_phase());
7753        assert_eq!(
7754            controller.phase(),
7755            crate::shutdown::ShutdownPhase::GracePeriod
7756        );
7757
7758        assert!(controller.advance_phase());
7759        assert_eq!(
7760            controller.phase(),
7761            crate::shutdown::ShutdownPhase::Cancelling
7762        );
7763
7764        assert!(controller.advance_phase());
7765        assert_eq!(
7766            controller.phase(),
7767            crate::shutdown::ShutdownPhase::RunningHooks
7768        );
7769
7770        assert!(controller.advance_phase());
7771        assert_eq!(controller.phase(), crate::shutdown::ShutdownPhase::Stopped);
7772
7773        // Can't go past Stopped
7774        assert!(!controller.advance_phase());
7775    }
7776
7777    #[test]
7778    #[serial(testing_network)]
7779    fn test_server_receiver_notified_on_shutdown() {
7780        let app = make_test_app();
7781        let server = TestServer::start(app);
7782
7783        let receiver = server.shutdown_controller().subscribe();
7784        assert!(!receiver.is_shutting_down());
7785
7786        server.shutdown();
7787        assert!(receiver.is_shutting_down());
7788        assert!(!receiver.is_forced());
7789    }
7790
7791    #[test]
7792    #[serial(testing_network)]
7793    fn test_server_forced_shutdown() {
7794        let app = make_test_app();
7795        let server = TestServer::start(app);
7796
7797        let receiver = server.shutdown_controller().subscribe();
7798
7799        // First shutdown -> graceful
7800        server.shutdown_controller().shutdown();
7801        assert!(receiver.is_shutting_down());
7802        assert!(!receiver.is_forced());
7803
7804        // Second shutdown -> forced
7805        server.shutdown_controller().shutdown();
7806        assert!(receiver.is_forced());
7807    }
7808
7809    #[test]
7810    #[serial(testing_network)]
7811    fn test_server_requests_work_before_shutdown_signal() {
7812        let app = make_test_app();
7813        let server = TestServer::start(app);
7814
7815        // Multiple requests work fine before any shutdown signal
7816        for i in 0..3 {
7817            let response = send_request(
7818                server.addr(),
7819                b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7820            );
7821            assert!(
7822                response.contains("200 OK"),
7823                "Request {i} should succeed before shutdown"
7824            );
7825        }
7826
7827        assert_eq!(server.request_count(), 3);
7828
7829        // Now shutdown
7830        server.shutdown();
7831        assert!(server.is_shutdown());
7832    }
7833}