1use 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#[derive(Debug, Clone, Default)]
63pub struct CookieJar {
64 cookies: HashMap<String, String>,
65}
66
67impl CookieJar {
68 #[must_use]
70 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
76 self.cookies.insert(name.into(), value.into());
77 }
78
79 #[must_use]
81 pub fn get(&self, name: &str) -> Option<&str> {
82 self.cookies.get(name).map(String::as_str)
83 }
84
85 pub fn remove(&mut self, name: &str) -> Option<String> {
87 self.cookies.remove(name)
88 }
89
90 pub fn clear(&mut self) {
92 self.cookies.clear();
93 }
94
95 #[must_use]
97 pub fn len(&self) -> usize {
98 self.cookies.len()
99 }
100
101 #[must_use]
103 pub fn is_empty(&self) -> bool {
104 self.cookies.is_empty()
105 }
106
107 #[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 pub fn parse_set_cookie(&mut self, header_value: &[u8]) {
125 if let Ok(value) = std::str::from_utf8(header_value) {
126 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
136pub 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 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 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 #[must_use]
223 pub fn seed(&self) -> Option<u64> {
224 self.seed
225 }
226
227 pub fn cookies(&self) -> parking_lot::MutexGuard<'_, CookieJar> {
232 self.cookies.lock()
233 }
234
235 pub fn clear_cookies(&self) {
237 self.cookies().clear();
238 }
239
240 #[must_use]
242 pub fn get(&self, path: &str) -> RequestBuilder<'_, H> {
243 RequestBuilder::new(self, Method::Get, path)
244 }
245
246 #[must_use]
248 pub fn post(&self, path: &str) -> RequestBuilder<'_, H> {
249 RequestBuilder::new(self, Method::Post, path)
250 }
251
252 #[must_use]
254 pub fn put(&self, path: &str) -> RequestBuilder<'_, H> {
255 RequestBuilder::new(self, Method::Put, path)
256 }
257
258 #[must_use]
260 pub fn delete(&self, path: &str) -> RequestBuilder<'_, H> {
261 RequestBuilder::new(self, Method::Delete, path)
262 }
263
264 #[must_use]
266 pub fn patch(&self, path: &str) -> RequestBuilder<'_, H> {
267 RequestBuilder::new(self, Method::Patch, path)
268 }
269
270 #[must_use]
272 pub fn options(&self, path: &str) -> RequestBuilder<'_, H> {
273 RequestBuilder::new(self, Method::Options, path)
274 }
275
276 #[must_use]
278 pub fn head(&self, path: &str) -> RequestBuilder<'_, H> {
279 RequestBuilder::new(self, Method::Head, path)
280 }
281
282 #[must_use]
284 pub fn request(&self, method: Method, path: &str) -> RequestBuilder<'_, H> {
285 RequestBuilder::new(self, method, path)
286 }
287
288 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 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 pub fn clear_dependency_overrides(&self) {
308 self.dependency_overrides.clear();
309 }
310
311 fn next_request_id(&self) -> u64 {
313 self.request_id_counter
314 .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
315 }
316
317 fn execute(&self, mut request: Request) -> TestResponse {
321 {
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 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 let response = futures_executor::block_on(self.handler.call(&ctx, &mut request));
340
341 {
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
367pub 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 fn new(client: &'a TestClient<H>, method: Method, path: &str) -> Self {
393 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 #[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 #[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 #[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 #[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 #[must_use]
463 pub fn body_str(self, body: &str) -> Self {
464 self.body(body.as_bytes().to_vec())
465 }
466
467 #[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 #[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 #[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#[derive(Debug)]
535pub struct TestResponse {
536 inner: Response,
537 request_id: u64,
538}
539
540impl TestResponse {
541 fn new(response: Response, request_id: u64) -> Self {
543 Self {
544 inner: response,
545 request_id,
546 }
547 }
548
549 #[must_use]
551 pub fn request_id(&self) -> u64 {
552 self.request_id
553 }
554
555 #[must_use]
557 pub fn status(&self) -> StatusCode {
558 self.inner.status()
559 }
560
561 #[must_use]
563 pub fn status_code(&self) -> u16 {
564 self.inner.status().as_u16()
565 }
566
567 #[must_use]
569 pub fn is_success(&self) -> bool {
570 let code = self.status_code();
571 (200..300).contains(&code)
572 }
573
574 #[must_use]
576 pub fn is_redirect(&self) -> bool {
577 let code = self.status_code();
578 (300..400).contains(&code)
579 }
580
581 #[must_use]
583 pub fn is_client_error(&self) -> bool {
584 let code = self.status_code();
585 (400..500).contains(&code)
586 }
587
588 #[must_use]
590 pub fn is_server_error(&self) -> bool {
591 let code = self.status_code();
592 (500..600).contains(&code)
593 }
594
595 #[must_use]
597 pub fn headers(&self) -> &[(String, Vec<u8>)] {
598 self.inner.headers()
599 }
600
601 #[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 #[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 #[must_use]
620 pub fn content_type(&self) -> Option<&str> {
621 self.header_str("content-type")
622 }
623
624 #[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 #[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 #[must_use]
648 pub fn text_opt(&self) -> Option<&str> {
649 std::str::from_utf8(self.bytes()).ok()
650 }
651
652 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
667 serde_json::from_slice(self.bytes())
668 }
669
670 #[must_use]
672 pub fn content_length(&self) -> usize {
673 self.bytes().len()
674 }
675
676 #[must_use]
678 pub fn into_inner(self) -> Response {
679 self.inner
680 }
681
682 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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
953pub 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 (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 (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 _ => {
1029 if actual == expected {
1030 Ok(())
1031 } else {
1032 Err(path.to_string())
1033 }
1034 }
1035 }
1036}
1037
1038pub 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
1062impl IntoStatusU16 for i32 {
1064 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1065 fn into_status_u16(self) -> u16 {
1066 self as u16
1068 }
1069}
1070
1071#[macro_export]
1094macro_rules! assert_status {
1095 ($response:expr, $expected:expr) => {{
1096 let response = &$response;
1097 let actual = response.status_code();
1098 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#[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#[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#[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#[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#[derive(Debug, Clone)]
1404pub struct LabTestConfig {
1405 pub seed: u64,
1407 pub chaos_enabled: bool,
1409 pub chaos_intensity: f64,
1411 pub max_steps: Option<u64>,
1413 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 #[must_use]
1432 pub fn new(seed: u64) -> Self {
1433 Self {
1434 seed,
1435 ..Default::default()
1436 }
1437 }
1438
1439 #[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 #[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 #[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 #[must_use]
1469 pub fn with_max_steps(mut self, max: u64) -> Self {
1470 self.max_steps = Some(max);
1471 self
1472 }
1473
1474 #[must_use]
1476 pub fn without_step_limit(mut self) -> Self {
1477 self.max_steps = None;
1478 self
1479 }
1480
1481 #[must_use]
1483 pub fn with_traces(mut self) -> Self {
1484 self.capture_traces = true;
1485 self
1486 }
1487}
1488
1489#[derive(Debug, Clone, Default)]
1493pub struct TestChaosStats {
1494 pub decision_points: u64,
1496 pub delays_injected: u64,
1498 pub cancellations_injected: u64,
1500 pub steps_executed: u64,
1502}
1503
1504impl TestChaosStats {
1505 #[must_use]
1507 #[allow(clippy::cast_precision_loss)] 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 #[must_use]
1519 pub fn had_chaos(&self) -> bool {
1520 self.delays_injected > 0 || self.cancellations_injected > 0
1521 }
1522}
1523
1524#[derive(Debug, Clone)]
1543pub struct MockTime {
1544 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 #[must_use]
1557 pub fn new() -> Self {
1558 Self {
1559 current_us: Arc::new(std::sync::atomic::AtomicU64::new(0)),
1560 }
1561 }
1562
1563 #[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 #[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 #[must_use]
1579 pub fn elapsed(&self) -> std::time::Duration {
1580 self.now()
1581 }
1582
1583 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 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 pub fn reset(&self) {
1601 self.current_us
1602 .store(0, std::sync::atomic::Ordering::Relaxed);
1603 }
1604}
1605
1606#[derive(Debug)]
1610pub struct CancellationTestResult {
1611 pub completed: bool,
1613 pub cancelled_at_checkpoint: bool,
1615 pub response: Option<Response>,
1617 pub cancellation_point: Option<String>,
1619}
1620
1621impl CancellationTestResult {
1622 #[must_use]
1624 pub fn gracefully_cancelled(&self) -> bool {
1625 !self.completed && self.cancelled_at_checkpoint
1626 }
1627
1628 #[must_use]
1630 pub fn completed_despite_cancel(&self) -> bool {
1631 self.completed
1632 }
1633}
1634
1635pub struct CancellationTest<H> {
1649 handler: H,
1650 seed: u64,
1651}
1652
1653impl<H: Handler + 'static> CancellationTest<H> {
1654 #[must_use]
1656 pub fn new(handler: H) -> Self {
1657 Self { handler, seed: 42 }
1658 }
1659
1660 #[must_use]
1662 pub fn with_seed(mut self, seed: u64) -> Self {
1663 self.seed = seed;
1664 self
1665 }
1666
1667 pub fn test_respects_cancellation(&self) -> CancellationTestResult {
1673 let cx = asupersync::Cx::for_testing();
1674 let ctx = RequestContext::new(cx, 1);
1675
1676 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 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 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 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 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 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 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 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 #[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 let _ = client.get("/set-cookie").send();
1977
1978 assert_eq!(client.cookies().get("session"), Some("abc123"));
1980
1981 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 assert_eq!(cloned.cookies().get("test"), Some("value"));
2017
2018 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 #[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 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 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 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 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 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 #[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 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 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 #[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 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 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 #[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 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 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 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 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 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 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 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 assert!(
2549 text.contains("dep1=0 dep2=0"),
2550 "Dependencies should be cached within request. Got: {}",
2551 text
2552 );
2553
2554 assert_eq!(CALL_COUNT.load(Ordering::SeqCst), 1);
2556 }
2557
2558 #[test]
2563 #[allow(clippy::float_cmp)] 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)] 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)] 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)] 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)] 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)] 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 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 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 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 #[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 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 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 let mut sum = 0;
2879 for i in 0..1000 {
2880 sum += i;
2881 }
2882 sum
2883 });
2884
2885 assert!(capture.passed());
2886 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 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 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
2991use 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#[derive(Debug, Clone)]
3006pub struct RecordedRequest {
3007 pub method: String,
3009 pub path: String,
3011 pub query: Option<String>,
3013 pub headers: Vec<(String, String)>,
3015 pub body: Vec<u8>,
3017 pub timestamp: std::time::Instant,
3019}
3020
3021impl RecordedRequest {
3022 #[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 #[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 #[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#[derive(Debug, Clone)]
3054pub struct MockResponse {
3055 pub status: u16,
3057 pub headers: Vec<(String, String)>,
3059 pub body: Vec<u8>,
3061 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 #[must_use]
3079 pub fn ok() -> Self {
3080 Self::default()
3081 }
3082
3083 #[must_use]
3085 pub fn with_status(status: u16) -> Self {
3086 Self {
3087 status,
3088 ..Default::default()
3089 }
3090 }
3091
3092 #[must_use]
3094 pub fn status(mut self, status: u16) -> Self {
3095 self.status = status;
3096 self
3097 }
3098
3099 #[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 #[must_use]
3108 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
3109 self.body = body.into();
3110 self
3111 }
3112
3113 #[must_use]
3115 pub fn body_str(self, body: &str) -> Self {
3116 self.body(body.as_bytes().to_vec())
3117 }
3118
3119 #[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 #[must_use]
3130 pub fn delay(mut self, duration: Duration) -> Self {
3131 self.delay = Some(duration);
3132 self
3133 }
3134
3135 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 response.push_str(&format!("content-length: {}\r\n", self.body.len()));
3156
3157 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
3170pub 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 #[must_use]
3224 pub fn start() -> Self {
3225 Self::start_with_options(MockServerOptions::default())
3226 }
3227
3228 #[must_use]
3230 pub fn start_with_options(options: MockServerOptions) -> Self {
3231 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 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 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 let requests = Arc::clone(&requests);
3291 let responses = Arc::clone(&responses);
3292 let default_response = Arc::clone(&default_response);
3293
3294 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 thread::sleep(Duration::from_millis(10));
3306 }
3307 Err(e) => {
3308 eprintln!("MockServer accept error: {}", e);
3309 break;
3310 }
3311 }
3312 }
3313 }
3314
3315 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 let _ = stream.set_read_timeout(Some(read_timeout));
3325
3326 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 let Some(recorded) = Self::parse_request(&buffer) else {
3340 return;
3341 };
3342
3343 {
3345 let mut reqs = requests.lock();
3346 reqs.push(recorded.clone());
3347 }
3348
3349 let response = {
3351 let resps = responses.lock();
3352 match resps.get(&recorded.path) {
3353 Some(r) => r.clone(),
3354 None => {
3355 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 if let Some(delay) = response.delay {
3373 thread::sleep(delay);
3374 }
3375
3376 let response_bytes = response.to_http_response();
3378 let _ = stream.write_all(&response_bytes);
3379 let _ = stream.flush();
3380 }
3381
3382 fn parse_request(data: &[u8]) -> Option<RecordedRequest> {
3384 let text = std::str::from_utf8(data).ok()?;
3385 let mut lines = text.lines();
3386
3387 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 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 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 let body = if content_length > 0 {
3426 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 #[must_use]
3460 pub fn addr(&self) -> SocketAddr {
3461 self.addr
3462 }
3463
3464 #[must_use]
3466 pub fn url(&self) -> String {
3467 format!("http://{}", self.addr)
3468 }
3469
3470 #[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 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 pub fn set_default_response(&self, response: MockResponse) {
3498 let mut default = self.default_response.lock();
3499 *default = response;
3500 }
3501
3502 #[must_use]
3504 pub fn requests(&self) -> Vec<RecordedRequest> {
3505 let requests = self.requests.lock();
3506 requests.clone()
3507 }
3508
3509 #[must_use]
3511 pub fn request_count(&self) -> usize {
3512 let requests = self.requests.lock();
3513 requests.len()
3514 }
3515
3516 #[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 #[must_use]
3529 pub fn last_request(&self) -> Option<RecordedRequest> {
3530 let requests = self.requests.lock();
3531 requests.last().cloned()
3532 }
3533
3534 pub fn clear_requests(&self) {
3536 let mut requests = self.requests.lock();
3537 requests.clear();
3538 }
3539
3540 pub fn clear_responses(&self) {
3542 let mut responses = self.responses.lock();
3543 responses.clear();
3544 }
3545
3546 pub fn reset(&self) {
3548 self.clear_requests();
3549 self.clear_responses();
3550 }
3551
3552 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 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 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 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 self.shutdown
3618 .store(true, std::sync::atomic::Ordering::Release);
3619
3620 if let Some(handle) = self.handle.take() {
3622 let _ = handle.join();
3623 }
3624 }
3625}
3626
3627#[derive(Debug, Clone)]
3629pub struct MockServerOptions {
3630 pub default_response: MockResponse,
3632 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 #[must_use]
3648 pub fn new() -> Self {
3649 Self::default()
3650 }
3651
3652 #[must_use]
3654 pub fn default_response(mut self, response: MockResponse) -> Self {
3655 self.default_response = response;
3656 self
3657 }
3658
3659 #[must_use]
3661 pub fn read_timeout(mut self, timeout: Duration) -> Self {
3662 self.read_timeout = timeout;
3663 self
3664 }
3665}
3666
3667#[derive(Debug, Clone)]
3676pub struct TestServerLogEntry {
3677 pub method: String,
3679 pub path: String,
3681 pub status: u16,
3683 pub duration: Duration,
3685 pub timestamp: std::time::Instant,
3687}
3688
3689#[derive(Debug, Clone)]
3691pub struct TestServerConfig {
3692 pub read_timeout: Duration,
3694 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 #[must_use]
3710 pub fn new() -> Self {
3711 Self::default()
3712 }
3713
3714 #[must_use]
3716 pub fn read_timeout(mut self, timeout: Duration) -> Self {
3717 self.read_timeout = timeout;
3718 self
3719 }
3720
3721 #[must_use]
3723 pub fn log_requests(mut self, log: bool) -> Self {
3724 self.log_requests = log;
3725 self
3726 }
3727}
3728
3729pub 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 #[must_use]
3807 pub fn start(app: crate::app::App) -> Self {
3808 Self::start_with_config(app, TestServerConfig::default())
3809 }
3810
3811 #[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 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 while let Some(hook) = controller.pop_hook() {
3867 hook.run();
3868 }
3869 break;
3870 }
3871
3872 match listener.accept() {
3873 Ok((stream, _peer)) => {
3874 let _guard = controller.track_request();
3876
3877 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 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 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 let Some(parsed) = Self::parse_raw_request(&buffer) else {
3915 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 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 if let Some(ref query) = parsed.query {
3940 request.set_query(Some(query.clone()));
3941 }
3942
3943 for (name, value) in &parsed.headers {
3945 request
3946 .headers_mut()
3947 .insert(name.clone(), value.as_bytes().to_vec());
3948 }
3949
3950 if !parsed.body.is_empty() {
3952 request.set_body(Body::Bytes(parsed.body.clone()));
3953 }
3954
3955 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 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 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 let response_bytes = Self::serialize_response(response);
3982 let _ = stream.write_all(&response_bytes);
3983 let _ = stream.flush();
3984 }
3985
3986 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 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 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 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 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 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 Vec::new()
4071 }
4072 };
4073
4074 let mut buf = Vec::with_capacity(512 + body_bytes.len());
4075
4076 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 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 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 buf.extend_from_slice(b"\r\n");
4103
4104 buf.extend_from_slice(&body_bytes);
4106
4107 buf
4108 }
4109
4110 #[must_use]
4112 pub fn addr(&self) -> SocketAddr {
4113 self.addr
4114 }
4115
4116 #[must_use]
4118 pub fn port(&self) -> u16 {
4119 self.addr.port()
4120 }
4121
4122 #[must_use]
4124 pub fn url(&self) -> String {
4125 format!("http://{}", self.addr)
4126 }
4127
4128 #[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 #[must_use]
4141 pub fn log_entries(&self) -> Vec<TestServerLogEntry> {
4142 self.log_entries.lock().clone()
4143 }
4144
4145 #[must_use]
4147 pub fn request_count(&self) -> usize {
4148 self.log_entries.lock().len()
4149 }
4150
4151 pub fn clear_logs(&self) {
4153 self.log_entries.lock().clear();
4154 }
4155
4156 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 #[must_use]
4171 pub fn shutdown_controller(&self) -> &crate::shutdown::ShutdownController {
4172 &self.shutdown_controller
4173 }
4174
4175 #[must_use]
4177 pub fn in_flight_count(&self) -> usize {
4178 self.shutdown_controller.in_flight_count()
4179 }
4180
4181 pub fn shutdown(&self) {
4187 self.shutdown_controller.shutdown();
4188 self.shutdown
4189 .store(true, std::sync::atomic::Ordering::Release);
4190 }
4191
4192 #[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
4209struct ParsedRequest {
4211 method: String,
4212 path: String,
4213 query: Option<String>,
4214 headers: Vec<(String, String)>,
4215 body: Vec<u8>,
4216}
4217
4218#[derive(Debug, Clone)]
4224pub enum E2EStepResult {
4225 Passed,
4227 Failed(String),
4229 Skipped,
4231}
4232
4233impl E2EStepResult {
4234 #[must_use]
4236 pub fn is_passed(&self) -> bool {
4237 matches!(self, Self::Passed)
4238 }
4239
4240 #[must_use]
4242 pub fn is_failed(&self) -> bool {
4243 matches!(self, Self::Failed(_))
4244 }
4245}
4246
4247#[derive(Debug, Clone)]
4249pub struct E2ECapture {
4250 pub method: String,
4252 pub path: String,
4254 pub request_headers: Vec<(String, String)>,
4256 pub request_body: Option<String>,
4258 pub response_status: u16,
4260 pub response_headers: Vec<(String, String)>,
4262 pub response_body: String,
4264}
4265
4266#[derive(Debug, Clone)]
4268pub struct E2EStep {
4269 pub name: String,
4271 pub started_at: std::time::Instant,
4273 pub duration: std::time::Duration,
4275 pub result: E2EStepResult,
4277 pub capture: Option<E2ECapture>,
4279}
4280
4281impl E2EStep {
4282 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 fn complete(&mut self, result: E2EStepResult) {
4295 self.duration = self.started_at.elapsed();
4296 self.result = result;
4297 }
4298}
4299
4300pub struct E2EScenario<H> {
4331 name: String,
4333 description: Option<String>,
4335 client: TestClient<H>,
4337 steps: Vec<E2EStep>,
4339 stop_on_failure: bool,
4341 has_failure: bool,
4343 log_buffer: Vec<String>,
4345}
4346
4347impl<H: Handler + 'static> E2EScenario<H> {
4348 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 #[must_use]
4364 pub fn description(mut self, desc: impl Into<String>) -> Self {
4365 self.description = Some(desc.into());
4366 self
4367 }
4368
4369 #[must_use]
4371 pub fn stop_on_failure(mut self, stop: bool) -> Self {
4372 self.stop_on_failure = stop;
4373 self
4374 }
4375
4376 pub fn client(&self) -> &TestClient<H> {
4378 &self.client
4379 }
4380
4381 pub fn client_mut(&mut self) -> &mut TestClient<H> {
4383 &mut self.client
4384 }
4385
4386 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 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 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 let client_ref = std::panic::AssertUnwindSafe(&self.client);
4419
4420 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 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 #[must_use]
4489 pub fn passed(&self) -> bool {
4490 !self.has_failure
4491 }
4492
4493 #[must_use]
4495 pub fn steps(&self) -> &[E2EStep] {
4496 &self.steps
4497 }
4498
4499 #[must_use]
4501 pub fn logs(&self) -> &[String] {
4502 &self.log_buffer
4503 }
4504
4505 #[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 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#[derive(Debug, Clone)]
4546pub struct E2EReport {
4547 pub scenario_name: String,
4549 pub description: Option<String>,
4551 pub passed: usize,
4553 pub failed: usize,
4555 pub skipped: usize,
4557 pub total_duration: std::time::Duration,
4559 pub steps: Vec<E2EStep>,
4561 pub logs: Vec<String>,
4563}
4564
4565impl E2EReport {
4566 #[must_use]
4568 pub fn to_text(&self) -> String {
4569 let mut output = String::new();
4570
4571 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 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 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 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 #[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 #[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
4767fn escape_json(s: &str) -> String {
4769 s.replace('\\', "\\\\")
4770 .replace('"', "\\\"")
4771 .replace('\n', "\\n")
4772 .replace('\r', "\\r")
4773 .replace('\t', "\\t")
4774}
4775
4776fn escape_html(s: &str) -> String {
4778 s.replace('&', "&")
4779 .replace('<', "<")
4780 .replace('>', ">")
4781 .replace('"', """)
4782}
4783
4784#[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
4839use crate::logging::{LogEntry, LogLevel};
4844
4845#[derive(Debug, Clone)]
4847pub struct CapturedLog {
4848 pub level: LogLevel,
4850 pub message: String,
4852 pub request_id: u64,
4854 pub captured_at: std::time::Instant,
4856 pub fields: Vec<(String, String)>,
4858 pub target: Option<String>,
4860}
4861
4862impl CapturedLog {
4863 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 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 #[must_use]
4889 pub fn contains(&self, text: &str) -> bool {
4890 self.message.contains(text)
4891 }
4892
4893 #[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#[derive(Debug, Clone)]
4941pub struct TestLogger {
4942 logs: Arc<Mutex<Vec<CapturedLog>>>,
4944 timings: Arc<Mutex<TestTimings>>,
4946 echo_logs: bool,
4948}
4949
4950#[derive(Debug, Clone, Default)]
4952pub struct TestTimings {
4953 pub setup: Option<std::time::Duration>,
4955 pub execute: Option<std::time::Duration>,
4957 pub teardown: Option<std::time::Duration>,
4959 phase_start: Option<std::time::Instant>,
4961}
4962
4963impl TestTimings {
4964 pub fn start_phase(&mut self) {
4966 self.phase_start = Some(std::time::Instant::now());
4967 }
4968
4969 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 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 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 #[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 #[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 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 pub fn with_echo() -> Self {
5023 let mut logger = Self::new();
5024 logger.echo_logs = true;
5025 logger
5026 }
5027
5028 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 pub fn log_entry(&self, entry: &LogEntry) {
5038 self.log(CapturedLog::from_entry(entry));
5039 }
5040
5041 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 #[must_use]
5048 pub fn logs(&self) -> Vec<CapturedLog> {
5049 self.logs.lock().clone()
5050 }
5051
5052 #[must_use]
5054 pub fn count(&self) -> usize {
5055 self.logs.lock().len()
5056 }
5057
5058 pub fn clear(&self) {
5060 self.logs.lock().clear();
5061 }
5062
5063 #[must_use]
5065 pub fn contains_message(&self, text: &str) -> bool {
5066 self.logs.lock().iter().any(|log| log.contains(text))
5067 }
5068
5069 #[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 #[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 #[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 #[must_use]
5110 pub fn timings(&self) -> TestTimings {
5111 self.timings.lock().clone()
5112 }
5113
5114 pub fn start_phase(&self) {
5116 self.timings.lock().start_phase();
5117 }
5118
5119 pub fn end_setup(&self) {
5121 self.timings.lock().end_setup();
5122 }
5123
5124 pub fn end_execute(&self) {
5126 self.timings.lock().end_execute();
5127 }
5128
5129 pub fn end_teardown(&self) {
5131 self.timings.lock().end_teardown();
5132 }
5133
5134 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 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 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 logger.start_phase();
5201 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| execute(&logger)));
5202 logger.end_execute();
5203
5204 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#[derive(Debug)]
5242pub struct LogCapture<T> {
5243 pub logs: Vec<CapturedLog>,
5245 pub timings: TestTimings,
5247 pub result: Option<T>,
5249 pub panic_info: Option<String>,
5251}
5252
5253impl<T> LogCapture<T> {
5254 #[must_use]
5256 pub fn passed(&self) -> bool {
5257 self.result.is_some()
5258 }
5259
5260 #[must_use]
5262 pub fn failed(&self) -> bool {
5263 self.panic_info.is_some()
5264 }
5265
5266 #[must_use]
5268 pub fn contains_message(&self, text: &str) -> bool {
5269 self.logs.iter().any(|log| log.contains(text))
5270 }
5271
5272 #[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 #[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 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 pub fn unwrap_or(self, default: T) -> T {
5319 self.result.unwrap_or(default)
5320 }
5321}
5322
5323#[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#[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#[derive(Debug)]
5382pub struct ResponseDiff {
5383 pub expected_status: u16,
5385 pub actual_status: u16,
5387 pub expected_body: Option<String>,
5389 pub actual_body: String,
5391 pub header_diffs: Vec<(String, Option<String>, Option<String>)>,
5393}
5394
5395impl ResponseDiff {
5396 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 #[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 #[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 #[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 #[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#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
5496pub struct ResponseSnapshot {
5497 pub status: u16,
5499 pub headers: Vec<(String, String)>,
5501 pub body: String,
5503 #[serde(skip_serializing_if = "Option::is_none")]
5505 pub body_json: Option<serde_json::Value>,
5506}
5507
5508impl ResponseSnapshot {
5509 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 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 #[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 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 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 #[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 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 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 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 match (&self.body_json, &other.body_json) {
5648 (Some(a), Some(b)) => a == b,
5649 _ => self.body == other.body,
5650 }
5651 }
5652}
5653
5654fn 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_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 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 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 assert_ne!(s1, s2);
5931 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 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 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 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 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 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 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 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 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 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 assert_eq!(escape_html("hello"), "hello");
6373 assert_eq!(escape_html("<script>"), "<script>");
6374 assert_eq!(escape_html("a&b"), "a&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
6394pub trait TestFixture: Sized + Send {
6428 fn setup() -> Self;
6430
6431 fn teardown(&mut self) {}
6435}
6436
6437pub struct FixtureGuard<F: TestFixture> {
6441 fixture: Option<F>,
6442}
6443
6444impl<F: TestFixture> FixtureGuard<F> {
6445 pub fn new() -> Self {
6447 Self {
6448 fixture: Some(F::setup()),
6449 }
6450 }
6451
6452 pub fn get(&self) -> &F {
6454 self.fixture.as_ref().unwrap()
6455 }
6456
6457 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
6491pub struct IntegrationTest<H: Handler + 'static> {
6537 name: String,
6539 client: TestClient<H>,
6541 fixtures: HashMap<std::any::TypeId, Box<dyn std::any::Any + Send>>,
6543 reset_hooks: Vec<Box<dyn Fn() + Send + Sync>>,
6545}
6546
6547impl<H: Handler + 'static> IntegrationTest<H> {
6548 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 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 #[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 #[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 pub fn run<F>(mut self, test_fn: F)
6593 where
6594 F: FnOnce(&IntegrationTestContext<'_, H>) + std::panic::UnwindSafe,
6595 {
6596 let ctx = IntegrationTestContext {
6598 name: &self.name,
6599 client: &self.client,
6600 fixtures: &self.fixtures,
6601 };
6602
6603 let ctx_ref = std::panic::AssertUnwindSafe(&ctx);
6605
6606 let result = std::panic::catch_unwind(|| {
6608 test_fn(&ctx_ref);
6609 });
6610
6611 for hook in &self.reset_hooks {
6613 hook();
6614 }
6615
6616 self.client.clear_cookies();
6618 self.client.clear_dependency_overrides();
6619
6620 self.fixtures.clear();
6622
6623 if let Err(e) = result {
6625 std::panic::resume_unwind(e);
6626 }
6627 }
6628}
6629
6630pub struct IntegrationTestContext<'a, H: Handler> {
6632 name: &'a str,
6634 client: &'a TestClient<H>,
6636 fixtures: &'a HashMap<std::any::TypeId, Box<dyn std::any::Any + Send>>,
6638}
6639
6640impl<'a, H: Handler + 'static> IntegrationTestContext<'a, H> {
6641 #[must_use]
6643 pub fn name(&self) -> &str {
6644 self.name
6645 }
6646
6647 #[must_use]
6649 pub fn client(&self) -> &TestClient<H> {
6650 self.client
6651 }
6652
6653 #[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 #[must_use]
6668 pub fn fixture_mut<F: TestFixture + 'static>(&self) -> Option<&mut F> {
6669 None }
6674
6675 pub fn get(&self, path: &str) -> RequestBuilder<'_, H> {
6679 self.client.get(path)
6680 }
6681
6682 pub fn post(&self, path: &str) -> RequestBuilder<'_, H> {
6684 self.client.post(path)
6685 }
6686
6687 pub fn put(&self, path: &str) -> RequestBuilder<'_, H> {
6689 self.client.put(path)
6690 }
6691
6692 pub fn delete(&self, path: &str) -> RequestBuilder<'_, H> {
6694 self.client.delete(path)
6695 }
6696
6697 pub fn patch(&self, path: &str) -> RequestBuilder<'_, H> {
6699 self.client.patch(path)
6700 }
6701
6702 pub fn options(&self, path: &str) -> RequestBuilder<'_, H> {
6704 self.client.options(path)
6705 }
6706
6707 pub fn request(&self, method: Method, path: &str) -> RequestBuilder<'_, H> {
6709 self.client.request(method, path)
6710 }
6711}
6712
6713#[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 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 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 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 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 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 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 assert!(logs[0].duration < Duration::from_secs(1));
6964 }
6965
6966 #[test]
6971 fn test_server_shutdown_controller_available() {
6972 let app = make_test_app();
6973 let server = TestServer::start(app);
6974
6975 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 assert!(!server.shutdown_controller().is_shutting_down());
6988
6989 server.shutdown();
6991
6992 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 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 server.shutdown();
7016
7017 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 assert_eq!(server.in_flight_count(), 0);
7031
7032 send_request(
7035 server.addr(),
7036 b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7037 );
7038
7039 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 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 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 server.shutdown();
7092
7093 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 server.shutdown();
7127 drop(server);
7128
7129 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 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 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 server.shutdown_controller().shutdown();
7202 assert!(receiver.is_shutting_down());
7203 assert!(!receiver.is_forced());
7204
7205 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 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 server.shutdown();
7231 assert!(server.is_shutdown());
7232 }
7233}