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