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: Vec<StoredCookie>,
65 next_id: u64,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69enum CookieSameSite {
70 Lax,
71 Strict,
72 None,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq)]
76enum CookieDomain {
77 Any,
79 HostOnly(String),
81 Domain(String),
83}
84
85#[derive(Debug, Clone)]
86struct StoredCookie {
87 id: u64,
88 name: String,
89 value: String,
90 domain: CookieDomain,
91 path: String,
92 secure: bool,
93 #[allow(dead_code)]
94 http_only: bool,
95 #[allow(dead_code)]
96 same_site: Option<CookieSameSite>,
97 expires_at: Option<std::time::SystemTime>,
98}
99
100impl CookieJar {
101 #[must_use]
103 pub fn new() -> Self {
104 Self::default()
105 }
106
107 pub fn set(&mut self, name: impl Into<String>, value: impl Into<String>) {
109 let name = name.into();
110 let value = value.into();
111
112 self.cookies
114 .retain(|c| !(c.name == name && c.domain == CookieDomain::Any));
115
116 let id = self.next_id;
117 self.next_id = self.next_id.wrapping_add(1);
118 self.cookies.push(StoredCookie {
119 id,
120 name,
121 value,
122 domain: CookieDomain::Any,
123 path: "/".to_string(),
124 secure: false,
125 http_only: false,
126 same_site: None,
127 expires_at: None,
128 });
129 }
130
131 #[must_use]
133 pub fn get(&self, name: &str) -> Option<&str> {
134 self.cookies
135 .iter()
136 .filter(|c| c.name == name)
137 .max_by_key(|c| c.id)
138 .map(|c| c.value.as_str())
139 }
140
141 pub fn remove(&mut self, name: &str) -> Option<String> {
143 let mut removed: Option<String> = None;
144 self.cookies.retain(|c| {
145 if c.name == name {
146 removed = Some(c.value.clone());
147 false
148 } else {
149 true
150 }
151 });
152 removed
153 }
154
155 pub fn clear(&mut self) {
157 self.cookies.clear();
158 self.next_id = 0;
159 }
160
161 #[must_use]
163 pub fn len(&self) -> usize {
164 self.cookies.len()
165 }
166
167 #[must_use]
169 pub fn is_empty(&self) -> bool {
170 self.cookies.is_empty()
171 }
172
173 #[must_use]
175 pub fn to_cookie_header(&self) -> Option<String> {
176 let mut by_name: HashMap<&str, &StoredCookie> = HashMap::new();
177 for c in &self.cookies {
178 match by_name.get(c.name.as_str()) {
179 Some(existing) if existing.id >= c.id => {}
180 _ => {
181 by_name.insert(c.name.as_str(), c);
182 }
183 }
184 }
185
186 if by_name.is_empty() {
187 return None;
188 }
189
190 Some(
191 by_name
192 .into_values()
193 .map(|c| format!("{}={}", c.name, c.value))
194 .collect::<Vec<_>>()
195 .join("; "),
196 )
197 }
198
199 #[must_use]
204 pub fn cookie_header_for_request(&self, request: &Request) -> Option<String> {
205 let host = request_host(request);
206 let path = request.path();
207 let is_secure = request_is_secure(request);
208 let now = std::time::SystemTime::now();
209
210 let mut selected: HashMap<&str, &StoredCookie> = HashMap::new();
212 for c in &self.cookies {
213 if c.secure && !is_secure {
214 continue;
215 }
216 if let Some(exp) = c.expires_at {
217 if exp <= now {
218 continue;
219 }
220 }
221 if !domain_matches(&c.domain, host.as_deref()) {
222 continue;
223 }
224 if !path_matches(&c.path, path) {
225 continue;
226 }
227
228 match selected.get(c.name.as_str()) {
229 None => {
230 selected.insert(c.name.as_str(), c);
231 }
232 Some(existing) => {
233 let a = (c.path.len(), c.id);
234 let b = (existing.path.len(), existing.id);
235 if a > b {
236 selected.insert(c.name.as_str(), c);
237 }
238 }
239 }
240 }
241
242 if selected.is_empty() {
243 return None;
244 }
245
246 Some(
247 selected
248 .into_values()
249 .map(|c| format!("{}={}", c.name, c.value))
250 .collect::<Vec<_>>()
251 .join("; "),
252 )
253 }
254
255 pub fn parse_set_cookie(&mut self, request: &Request, header_value: &[u8]) {
257 let Ok(value) = std::str::from_utf8(header_value) else {
258 return;
259 };
260 self.parse_set_cookie_str(request, value);
261 }
262
263 fn parse_set_cookie_str(&mut self, request: &Request, value: &str) {
264 let mut parts = value.split(';');
265 let Some((name, val)) = parse_cookie_name_value(parts.next()) else {
266 return;
267 };
268
269 let host = request_host(request);
270 let attrs = parse_set_cookie_attrs(parts);
271
272 let Some(domain) = cookie_domain_for_set_cookie(host.as_deref(), attrs.domain) else {
273 return;
274 };
275
276 let path = attrs
277 .path
278 .unwrap_or_else(|| default_cookie_path(request.path()));
279
280 let expires_at = match compute_cookie_expiration(attrs.max_age, attrs.expires_at) {
281 CookieExpiration::Delete => {
282 self.remove_by_key(name, &domain, &path);
283 return;
284 }
285 CookieExpiration::Keep(expires_at) => expires_at,
286 };
287
288 let id = self.next_id;
289 self.next_id = self.next_id.wrapping_add(1);
290 self.upsert(StoredCookie {
291 id,
292 name: name.to_string(),
293 value: val.to_string(),
294 domain,
295 path,
296 secure: attrs.secure,
297 http_only: attrs.http_only,
298 same_site: attrs.same_site,
299 expires_at,
300 });
301 }
302
303 fn remove_by_key(&mut self, name: &str, domain: &CookieDomain, path: &str) {
304 self.cookies
305 .retain(|c| !(c.name == name && &c.domain == domain && c.path == path));
306 }
307
308 fn upsert(&mut self, cookie: StoredCookie) {
309 for existing in &mut self.cookies {
311 if existing.name == cookie.name
312 && existing.domain == cookie.domain
313 && existing.path == cookie.path
314 {
315 *existing = cookie;
316 return;
317 }
318 }
319 self.cookies.push(cookie);
320 }
321}
322
323#[derive(Debug, Default)]
324struct SetCookieAttrs {
325 domain: Option<String>,
326 path: Option<String>,
327 max_age: Option<i64>,
328 secure: bool,
329 http_only: bool,
330 same_site: Option<CookieSameSite>,
331 expires_at: Option<std::time::SystemTime>,
332}
333
334#[derive(Debug, Clone, Copy)]
335enum CookieExpiration {
336 Delete,
337 Keep(Option<std::time::SystemTime>),
338}
339
340fn parse_cookie_name_value(first: Option<&str>) -> Option<(&str, &str)> {
341 let first = first?;
342 let (name, val) = first.split_once('=')?;
343 let name = name.trim();
344 if name.is_empty() {
345 return None;
346 }
347 Some((name, val.trim()))
348}
349
350fn parse_set_cookie_attrs<'a>(parts: impl Iterator<Item = &'a str>) -> SetCookieAttrs {
351 let mut attrs = SetCookieAttrs::default();
352 for raw in parts {
353 let raw = raw.trim();
354 if raw.is_empty() {
355 continue;
356 }
357 if let Some((k, v)) = raw.split_once('=') {
358 let k = k.trim().to_ascii_lowercase();
359 let v = v.trim();
360 match k.as_str() {
361 "domain" => {
362 let mut d = v.trim_matches('"').trim().to_ascii_lowercase();
363 if let Some(stripped) = d.strip_prefix('.') {
364 d = stripped.to_string();
365 }
366 if !d.is_empty() {
367 attrs.domain = Some(d);
368 }
369 }
370 "path" => {
371 let p = v.trim_matches('"').trim();
372 if p.starts_with('/') {
373 attrs.path = Some(p.to_string());
374 }
375 }
376 "max-age" => {
377 if let Ok(n) = v.parse::<i64>() {
378 attrs.max_age = Some(n);
379 }
380 }
381 "samesite" => {
382 let ss = v.trim_matches('"').trim();
383 attrs.same_site = match ss.to_ascii_lowercase().as_str() {
384 "lax" => Some(CookieSameSite::Lax),
385 "strict" => Some(CookieSameSite::Strict),
386 "none" => Some(CookieSameSite::None),
387 _ => None,
388 };
389 }
390 "expires" => {
391 if let Some(t) = parse_http_date(v) {
392 attrs.expires_at = Some(t);
393 }
394 }
395 _ => {}
396 }
397 } else {
398 match raw.to_ascii_lowercase().as_str() {
399 "secure" => attrs.secure = true,
400 "httponly" => attrs.http_only = true,
401 _ => {}
402 }
403 }
404 }
405 attrs
406}
407
408fn cookie_domain_for_set_cookie(
409 host: Option<&str>,
410 domain_attr: Option<String>,
411) -> Option<CookieDomain> {
412 match domain_attr {
413 Some(d) => {
414 let h = host?;
415 let domain = CookieDomain::Domain(d);
416 if domain_matches(&domain, Some(h)) {
417 Some(domain)
418 } else {
419 None
420 }
421 }
422 None => {
423 let h = host?;
424 Some(CookieDomain::HostOnly(h.to_string()))
425 }
426 }
427}
428
429fn compute_cookie_expiration(
430 max_age: Option<i64>,
431 mut expires_at: Option<std::time::SystemTime>,
432) -> CookieExpiration {
433 let now = std::time::SystemTime::now();
434 if let Some(n) = max_age {
435 if n <= 0 {
436 return CookieExpiration::Delete;
437 }
438 let Ok(secs) = u64::try_from(n) else {
439 return CookieExpiration::Delete;
440 };
441 expires_at = now.checked_add(std::time::Duration::from_secs(secs));
442 }
443 if let Some(exp) = expires_at {
444 if exp <= now {
445 return CookieExpiration::Delete;
446 }
447 }
448 CookieExpiration::Keep(expires_at)
449}
450
451fn request_host(request: &Request) -> Option<String> {
452 let host = request.headers().get("host")?;
453 let s = std::str::from_utf8(host).ok()?;
454 let host = s.trim();
455 if host.is_empty() {
456 return None;
457 }
458 Some(host.split(':').next().unwrap_or(host).to_ascii_lowercase())
460}
461
462fn request_is_secure(request: &Request) -> bool {
463 if let Some(info) = request.get_extension::<crate::request::ConnectionInfo>() {
464 if info.is_tls {
465 return true;
466 }
467 }
468
469 if let Some(forwarded) = request.headers().get("forwarded") {
470 if let Ok(s) = std::str::from_utf8(forwarded) {
471 for entry in s.split(',') {
472 for param in entry.split(';') {
473 let param = param.trim();
474 if let Some((k, v)) = param.split_once('=') {
475 if k.trim().eq_ignore_ascii_case("proto") {
476 let proto = v.trim().trim_matches('"');
477 if proto.eq_ignore_ascii_case("https") {
478 return true;
479 }
480 }
481 }
482 }
483 }
484 }
485 }
486
487 if let Some(proto) = request.headers().get("x-forwarded-proto") {
488 let first = proto.split(|&b| b == b',').next().unwrap_or(proto);
489 let first = trim_ascii_bytes(first);
490 return first.eq_ignore_ascii_case(b"https");
491 }
492 if let Some(ssl) = request.headers().get("x-forwarded-ssl") {
493 return ssl.eq_ignore_ascii_case(b"on");
494 }
495 if let Some(https) = request.headers().get("front-end-https") {
496 return https.eq_ignore_ascii_case(b"on");
497 }
498
499 false
500}
501
502fn trim_ascii_bytes(mut bytes: &[u8]) -> &[u8] {
503 while matches!(bytes.first(), Some(b' ' | b'\t')) {
504 bytes = &bytes[1..];
505 }
506 while matches!(bytes.last(), Some(b' ' | b'\t')) {
507 bytes = &bytes[..bytes.len() - 1];
508 }
509 bytes
510}
511
512fn default_cookie_path(request_path: &str) -> String {
513 if !request_path.starts_with('/') {
515 return "/".to_string();
516 }
517 if request_path == "/" {
518 return "/".to_string();
519 }
520 match request_path.rfind('/') {
521 Some(0) | None => "/".to_string(),
522 Some(idx) => request_path[..idx].to_string(),
523 }
524}
525
526fn domain_matches(domain: &CookieDomain, host: Option<&str>) -> bool {
527 match domain {
528 CookieDomain::Any => true,
529 CookieDomain::HostOnly(d) => host.is_some_and(|h| h.eq_ignore_ascii_case(d)),
530 CookieDomain::Domain(d) => {
531 let Some(h) = host else { return false };
532 if h.eq_ignore_ascii_case(d) {
533 return true;
534 }
535 h.len() > d.len() && h.ends_with(d) && h.as_bytes()[h.len() - d.len() - 1] == b'.'
537 }
538 }
539}
540
541fn path_matches(cookie_path: &str, request_path: &str) -> bool {
542 if cookie_path == "/" {
543 return request_path.starts_with('/');
544 }
545 if !request_path.starts_with(cookie_path) {
546 return false;
547 }
548 if cookie_path.ends_with('/') {
549 return true;
550 }
551 request_path
552 .as_bytes()
553 .get(cookie_path.len())
554 .is_none_or(|&b| b == b'/')
555}
556
557fn parse_http_date(input: &str) -> Option<std::time::SystemTime> {
558 let s = input.trim().trim_matches('"').trim();
561 let (_dow, rest) = s.split_once(',')?;
562 let rest = rest.trim();
563 let mut it = rest.split_whitespace();
564 let day = it.next()?.parse::<u32>().ok()?;
565 let month = match it.next()? {
566 "Jan" => 1,
567 "Feb" => 2,
568 "Mar" => 3,
569 "Apr" => 4,
570 "May" => 5,
571 "Jun" => 6,
572 "Jul" => 7,
573 "Aug" => 8,
574 "Sep" => 9,
575 "Oct" => 10,
576 "Nov" => 11,
577 "Dec" => 12,
578 _ => return None,
579 };
580 let year = it.next()?.parse::<i32>().ok()?;
581 let time = it.next()?;
582 let tz = it.next()?;
583 if tz != "GMT" {
584 return None;
585 }
586 let (hh, mm, ss) = {
587 let mut t = time.split(':');
588 let hh = t.next()?.parse::<u32>().ok()?;
589 let mm = t.next()?.parse::<u32>().ok()?;
590 let ss = t.next()?.parse::<u32>().ok()?;
591 (hh, mm, ss)
592 };
593
594 fn days_from_civil(y: i32, m: u32, d: u32) -> i64 {
596 let y = i64::from(y) - i64::from(m <= 2);
598 let era = (if y >= 0 { y } else { y - 399 }) / 400;
599 let yoe = y - era * 400;
600 let m = i64::from(m);
601 let doy = (153 * (m + if m > 2 { -3 } else { 9 }) + 2) / 5 + i64::from(d) - 1;
602 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
603 era * 146097 + doe - 719468
604 }
605
606 let days = days_from_civil(year, month, day);
607 let secs = days
608 .checked_mul(86_400)?
609 .checked_add(i64::from(hh) * 3600 + i64::from(mm) * 60 + i64::from(ss))?;
610 if secs < 0 {
611 return None;
612 }
613 let secs_u64 = u64::try_from(secs).ok()?;
614 Some(std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs_u64))
615}
616
617pub struct TestClient<H> {
651 handler: Arc<H>,
652 cookies: Arc<Mutex<CookieJar>>,
653 dependency_overrides: Arc<DependencyOverrides>,
654 seed: Option<u64>,
655 request_id_counter: Arc<std::sync::atomic::AtomicU64>,
656}
657
658impl<H: Handler + 'static> TestClient<H> {
659 pub fn new(handler: H) -> Self {
667 let dependency_overrides = handler
668 .dependency_overrides()
669 .unwrap_or_else(|| Arc::new(DependencyOverrides::new()));
670 Self {
671 handler: Arc::new(handler),
672 cookies: Arc::new(Mutex::new(CookieJar::new())),
673 dependency_overrides,
674 seed: None,
675 request_id_counter: Arc::new(std::sync::atomic::AtomicU64::new(1)),
676 }
677 }
678
679 pub fn with_seed(handler: H, seed: u64) -> Self {
690 let dependency_overrides = handler
691 .dependency_overrides()
692 .unwrap_or_else(|| Arc::new(DependencyOverrides::new()));
693 Self {
694 handler: Arc::new(handler),
695 cookies: Arc::new(Mutex::new(CookieJar::new())),
696 dependency_overrides,
697 seed: Some(seed),
698 request_id_counter: Arc::new(std::sync::atomic::AtomicU64::new(1)),
699 }
700 }
701
702 #[must_use]
704 pub fn seed(&self) -> Option<u64> {
705 self.seed
706 }
707
708 pub fn cookies(&self) -> parking_lot::MutexGuard<'_, CookieJar> {
713 self.cookies.lock()
714 }
715
716 pub fn clear_cookies(&self) {
718 self.cookies().clear();
719 }
720
721 #[must_use]
723 pub fn get(&self, path: &str) -> RequestBuilder<'_, H> {
724 RequestBuilder::new(self, Method::Get, path)
725 }
726
727 #[must_use]
729 pub fn post(&self, path: &str) -> RequestBuilder<'_, H> {
730 RequestBuilder::new(self, Method::Post, path)
731 }
732
733 #[must_use]
735 pub fn put(&self, path: &str) -> RequestBuilder<'_, H> {
736 RequestBuilder::new(self, Method::Put, path)
737 }
738
739 #[must_use]
741 pub fn delete(&self, path: &str) -> RequestBuilder<'_, H> {
742 RequestBuilder::new(self, Method::Delete, path)
743 }
744
745 #[must_use]
747 pub fn patch(&self, path: &str) -> RequestBuilder<'_, H> {
748 RequestBuilder::new(self, Method::Patch, path)
749 }
750
751 #[must_use]
753 pub fn options(&self, path: &str) -> RequestBuilder<'_, H> {
754 RequestBuilder::new(self, Method::Options, path)
755 }
756
757 #[must_use]
759 pub fn head(&self, path: &str) -> RequestBuilder<'_, H> {
760 RequestBuilder::new(self, Method::Head, path)
761 }
762
763 #[must_use]
765 pub fn request(&self, method: Method, path: &str) -> RequestBuilder<'_, H> {
766 RequestBuilder::new(self, method, path)
767 }
768
769 pub fn override_dependency<T, F, Fut>(&self, f: F)
771 where
772 T: FromDependency,
773 F: Fn(&RequestContext, &mut Request) -> Fut + Send + Sync + 'static,
774 Fut: Future<Output = Result<T, T::Error>> + Send + 'static,
775 {
776 self.dependency_overrides.insert::<T, F, Fut>(f);
777 }
778
779 pub fn override_dependency_value<T>(&self, value: T)
781 where
782 T: FromDependency,
783 {
784 self.dependency_overrides.insert_value(value);
785 }
786
787 pub fn clear_dependency_overrides(&self) {
789 self.dependency_overrides.clear();
790 }
791
792 fn next_request_id(&self) -> u64 {
794 self.request_id_counter
795 .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
796 }
797
798 fn execute(&self, mut request: Request) -> TestResponse {
802 if !request.headers().contains("host") {
805 request.headers_mut().insert("host", b"testserver".to_vec());
806 }
807
808 {
810 let jar = self.cookies();
811 if let Some(cookie_header) = jar.cookie_header_for_request(&request) {
812 request
813 .headers_mut()
814 .insert("cookie", cookie_header.into_bytes());
815 }
816 }
817
818 let cx = Cx::for_testing();
820 let request_id = self.next_request_id();
821 let ctx =
822 RequestContext::with_overrides(cx, request_id, Arc::clone(&self.dependency_overrides));
823
824 let response = futures_executor::block_on(self.handler.call(&ctx, &mut request));
826
827 {
829 let mut jar = self.cookies();
830 for (name, value) in response.headers() {
831 if name.eq_ignore_ascii_case("set-cookie") {
832 jar.parse_set_cookie(&request, value);
833 }
834 }
835 }
836
837 TestResponse::new(response, request_id)
838 }
839}
840
841impl<H> Clone for TestClient<H> {
842 fn clone(&self) -> Self {
843 Self {
844 handler: Arc::clone(&self.handler),
845 cookies: Arc::clone(&self.cookies),
846 dependency_overrides: Arc::clone(&self.dependency_overrides),
847 seed: self.seed,
848 request_id_counter: Arc::clone(&self.request_id_counter),
849 }
850 }
851}
852
853pub struct RequestBuilder<'a, H> {
868 client: &'a TestClient<H>,
869 method: Method,
870 path: String,
871 query: Option<String>,
872 headers: Vec<(String, Vec<u8>)>,
873 body: Body,
874}
875
876impl<'a, H: Handler + 'static> RequestBuilder<'a, H> {
877 fn new(client: &'a TestClient<H>, method: Method, path: &str) -> Self {
879 let (path, query) = if let Some(idx) = path.find('?') {
881 (path[..idx].to_string(), Some(path[idx + 1..].to_string()))
882 } else {
883 (path.to_string(), None)
884 };
885
886 Self {
887 client,
888 method,
889 path,
890 query,
891 headers: Vec::new(),
892 body: Body::Empty,
893 }
894 }
895
896 #[must_use]
906 pub fn query(mut self, key: &str, value: &str) -> Self {
907 let param = format!("{key}={value}");
908 self.query = Some(match self.query {
909 Some(q) => format!("{q}&{param}"),
910 None => param,
911 });
912 self
913 }
914
915 #[must_use]
923 pub fn header(mut self, name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
924 self.headers.push((name.into(), value.into()));
925 self
926 }
927
928 #[must_use]
930 pub fn header_str(self, name: impl Into<String>, value: &str) -> Self {
931 self.header(name, value.as_bytes().to_vec())
932 }
933
934 #[must_use]
942 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
943 self.body = Body::Bytes(body.into());
944 self
945 }
946
947 #[must_use]
949 pub fn body_str(self, body: &str) -> Self {
950 self.body(body.as_bytes().to_vec())
951 }
952
953 #[must_use]
966 pub fn json<T: serde::Serialize>(mut self, value: &T) -> Self {
967 let bytes = serde_json::to_vec(value).expect("JSON serialization failed");
968 self.body = Body::Bytes(bytes);
969 self.headers
970 .push(("content-type".to_string(), b"application/json".to_vec()));
971 self
972 }
973
974 #[must_use]
978 pub fn cookie(self, name: &str, value: &str) -> Self {
979 let cookie = format!("{name}={value}");
980 self.header("cookie", cookie.into_bytes())
981 }
982
983 #[must_use]
991 pub fn send(self) -> TestResponse {
992 let mut request = Request::new(self.method, self.path);
993 request.set_query(self.query);
994 request.set_body(self.body);
995
996 for (name, value) in self.headers {
997 request.headers_mut().insert(name, value);
998 }
999
1000 self.client.execute(request)
1001 }
1002}
1003
1004#[derive(Debug)]
1021pub struct TestResponse {
1022 inner: Response,
1023 request_id: u64,
1024}
1025
1026impl TestResponse {
1027 fn new(response: Response, request_id: u64) -> Self {
1029 Self {
1030 inner: response,
1031 request_id,
1032 }
1033 }
1034
1035 #[must_use]
1037 pub fn request_id(&self) -> u64 {
1038 self.request_id
1039 }
1040
1041 #[must_use]
1043 pub fn status(&self) -> StatusCode {
1044 self.inner.status()
1045 }
1046
1047 #[must_use]
1049 pub fn status_code(&self) -> u16 {
1050 self.inner.status().as_u16()
1051 }
1052
1053 #[must_use]
1055 pub fn is_success(&self) -> bool {
1056 let code = self.status_code();
1057 (200..300).contains(&code)
1058 }
1059
1060 #[must_use]
1062 pub fn is_redirect(&self) -> bool {
1063 let code = self.status_code();
1064 (300..400).contains(&code)
1065 }
1066
1067 #[must_use]
1069 pub fn is_client_error(&self) -> bool {
1070 let code = self.status_code();
1071 (400..500).contains(&code)
1072 }
1073
1074 #[must_use]
1076 pub fn is_server_error(&self) -> bool {
1077 let code = self.status_code();
1078 (500..600).contains(&code)
1079 }
1080
1081 #[must_use]
1083 pub fn headers(&self) -> &[(String, Vec<u8>)] {
1084 self.inner.headers()
1085 }
1086
1087 #[must_use]
1089 pub fn header(&self, name: &str) -> Option<&[u8]> {
1090 let name_lower = name.to_ascii_lowercase();
1091 self.inner
1092 .headers()
1093 .iter()
1094 .find(|(n, _)| n.to_ascii_lowercase() == name_lower)
1095 .map(|(_, v)| v.as_slice())
1096 }
1097
1098 #[must_use]
1100 pub fn header_str(&self, name: &str) -> Option<&str> {
1101 self.header(name).and_then(|v| std::str::from_utf8(v).ok())
1102 }
1103
1104 #[must_use]
1106 pub fn content_type(&self) -> Option<&str> {
1107 self.header_str("content-type")
1108 }
1109
1110 #[must_use]
1112 pub fn bytes(&self) -> &[u8] {
1113 match self.inner.body_ref() {
1114 ResponseBody::Empty => &[],
1115 ResponseBody::Bytes(b) => b,
1116 ResponseBody::Stream(_) => {
1117 panic!("streaming response body not supported in TestResponse")
1118 }
1119 }
1120 }
1121
1122 #[must_use]
1128 pub fn text(&self) -> &str {
1129 std::str::from_utf8(self.bytes()).expect("response body is not valid UTF-8")
1130 }
1131
1132 #[must_use]
1134 pub fn text_opt(&self) -> Option<&str> {
1135 std::str::from_utf8(self.bytes()).ok()
1136 }
1137
1138 pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
1153 serde_json::from_slice(self.bytes())
1154 }
1155
1156 #[must_use]
1158 pub fn content_length(&self) -> usize {
1159 self.bytes().len()
1160 }
1161
1162 #[must_use]
1164 pub fn into_inner(self) -> Response {
1165 self.inner
1166 }
1167
1168 #[must_use]
1178 pub fn assert_status(&self, expected: StatusCode) -> &Self {
1179 assert_eq!(
1180 self.status(),
1181 expected,
1182 "Expected status {}, got {} for request {}",
1183 expected.as_u16(),
1184 self.status_code(),
1185 self.request_id
1186 );
1187 self
1188 }
1189
1190 #[must_use]
1196 pub fn assert_status_code(&self, expected: u16) -> &Self {
1197 assert_eq!(
1198 self.status_code(),
1199 expected,
1200 "Expected status {expected}, got {} for request {}",
1201 self.status_code(),
1202 self.request_id
1203 );
1204 self
1205 }
1206
1207 #[must_use]
1213 pub fn assert_success(&self) -> &Self {
1214 assert!(
1215 self.is_success(),
1216 "Expected success status, got {} for request {}",
1217 self.status_code(),
1218 self.request_id
1219 );
1220 self
1221 }
1222
1223 #[must_use]
1229 pub fn assert_header(&self, name: &str, expected: &str) -> &Self {
1230 let actual = self.header_str(name);
1231 assert_eq!(
1232 actual,
1233 Some(expected),
1234 "Expected header '{name}' to be '{expected}', got {:?} for request {}",
1235 actual,
1236 self.request_id
1237 );
1238 self
1239 }
1240
1241 #[must_use]
1247 pub fn assert_text(&self, expected: &str) -> &Self {
1248 assert_eq!(
1249 self.text(),
1250 expected,
1251 "Body mismatch for request {}",
1252 self.request_id
1253 );
1254 self
1255 }
1256
1257 #[must_use]
1263 pub fn assert_text_contains(&self, expected: &str) -> &Self {
1264 assert!(
1265 self.text().contains(expected),
1266 "Expected body to contain '{}', got '{}' for request {}",
1267 expected,
1268 self.text(),
1269 self.request_id
1270 );
1271 self
1272 }
1273
1274 #[must_use]
1280 pub fn assert_json<T>(&self, expected: &T) -> &Self
1281 where
1282 T: serde::de::DeserializeOwned + serde::Serialize + PartialEq + std::fmt::Debug,
1283 {
1284 let actual: T = self.json().expect("Failed to parse response as JSON");
1285 assert_eq!(
1286 actual, *expected,
1287 "JSON body mismatch for request {}",
1288 self.request_id
1289 );
1290 self
1291 }
1292
1293 #[must_use]
1311 pub fn assert_json_contains(&self, expected: &serde_json::Value) -> &Self {
1312 let actual: serde_json::Value = self.json().expect("Failed to parse response as JSON");
1313 if let Err(path) = json_contains(&actual, expected) {
1314 panic!(
1315 "JSON partial match failed at path '{}' for request {}\n\
1316 Expected (partial):\n{}\n\
1317 Actual:\n{}",
1318 path,
1319 self.request_id,
1320 serde_json::to_string_pretty(expected).unwrap_or_else(|_| format!("{expected:?}")),
1321 serde_json::to_string_pretty(&actual).unwrap_or_else(|_| format!("{actual:?}")),
1322 );
1323 }
1324 self
1325 }
1326
1327 #[cfg(feature = "regex")]
1339 #[must_use]
1340 pub fn assert_body_matches(&self, pattern: &str) -> &Self {
1341 let re = regex::Regex::new(pattern)
1342 .unwrap_or_else(|e| panic!("Invalid regex pattern '{pattern}': {e}"));
1343 let body = self.text();
1344 assert!(
1345 re.is_match(body),
1346 "Expected body to match pattern '{}', got '{}' for request {}",
1347 pattern,
1348 body,
1349 self.request_id
1350 );
1351 self
1352 }
1353
1354 #[cfg(feature = "regex")]
1361 #[must_use]
1362 pub fn assert_header_matches(&self, name: &str, pattern: &str) -> &Self {
1363 let re = regex::Regex::new(pattern)
1364 .unwrap_or_else(|e| panic!("Invalid regex pattern '{pattern}': {e}"));
1365 let value = self
1366 .header_str(name)
1367 .unwrap_or_else(|| panic!("Header '{name}' not found for request {}", self.request_id));
1368 assert!(
1369 re.is_match(value),
1370 "Expected header '{}' to match pattern '{}', got '{}' for request {}",
1371 name,
1372 pattern,
1373 value,
1374 self.request_id
1375 );
1376 self
1377 }
1378
1379 #[must_use]
1385 pub fn assert_header_exists(&self, name: &str) -> &Self {
1386 assert!(
1387 self.header(name).is_some(),
1388 "Expected header '{}' to exist for request {}",
1389 name,
1390 self.request_id
1391 );
1392 self
1393 }
1394
1395 #[must_use]
1401 pub fn assert_header_missing(&self, name: &str) -> &Self {
1402 assert!(
1403 self.header(name).is_none(),
1404 "Expected header '{}' to not exist for request {}, but found {:?}",
1405 name,
1406 self.request_id,
1407 self.header_str(name)
1408 );
1409 self
1410 }
1411
1412 #[must_use]
1421 pub fn assert_content_type_contains(&self, expected: &str) -> &Self {
1422 let ct = self.content_type().unwrap_or_else(|| {
1423 panic!(
1424 "Content-Type header not found for request {}",
1425 self.request_id
1426 )
1427 });
1428 assert!(
1429 ct.contains(expected),
1430 "Expected Content-Type to contain '{}', got '{}' for request {}",
1431 expected,
1432 ct,
1433 self.request_id
1434 );
1435 self
1436 }
1437}
1438
1439pub fn json_contains(
1471 actual: &serde_json::Value,
1472 expected: &serde_json::Value,
1473) -> Result<(), String> {
1474 json_contains_at_path(actual, expected, "$")
1475}
1476
1477fn json_contains_at_path(
1478 actual: &serde_json::Value,
1479 expected: &serde_json::Value,
1480 path: &str,
1481) -> Result<(), String> {
1482 use serde_json::Value;
1483
1484 match (actual, expected) {
1485 (Value::Object(actual_obj), Value::Object(expected_obj)) => {
1487 for (key, expected_val) in expected_obj {
1488 let child_path = format!("{path}.{key}");
1489 match actual_obj.get(key) {
1490 Some(actual_val) => {
1491 json_contains_at_path(actual_val, expected_val, &child_path)?;
1492 }
1493 None => {
1494 return Err(child_path);
1495 }
1496 }
1497 }
1498 Ok(())
1499 }
1500 (Value::Array(actual_arr), Value::Array(expected_arr)) => {
1502 if actual_arr.len() != expected_arr.len() {
1503 return Err(format!("{path}[length]"));
1504 }
1505 for (i, (actual_elem, expected_elem)) in
1506 actual_arr.iter().zip(expected_arr.iter()).enumerate()
1507 {
1508 let child_path = format!("{path}[{i}]");
1509 json_contains_at_path(actual_elem, expected_elem, &child_path)?;
1510 }
1511 Ok(())
1512 }
1513 _ => {
1515 if actual == expected {
1516 Ok(())
1517 } else {
1518 Err(path.to_string())
1519 }
1520 }
1521 }
1522}
1523
1524pub trait IntoStatusU16 {
1533 fn into_status_u16(self) -> u16;
1534}
1535
1536impl IntoStatusU16 for u16 {
1537 fn into_status_u16(self) -> u16 {
1538 self
1539 }
1540}
1541
1542impl IntoStatusU16 for StatusCode {
1543 fn into_status_u16(self) -> u16 {
1544 self.as_u16()
1545 }
1546}
1547
1548impl IntoStatusU16 for i32 {
1550 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1551 fn into_status_u16(self) -> u16 {
1552 self as u16
1554 }
1555}
1556
1557#[macro_export]
1580macro_rules! assert_status {
1581 ($response:expr, $expected:expr) => {{
1582 let response = &$response;
1583 let actual = response.status_code();
1584 let expected_code: u16 = $crate::testing::IntoStatusU16::into_status_u16($expected);
1586 if actual != expected_code {
1587 panic!(
1588 "assertion failed: `(response.status() == {})`\n\
1589 expected status: {}\n\
1590 actual status: {}\n\
1591 request id: {}\n\
1592 response body: {}",
1593 expected_code,
1594 expected_code,
1595 actual,
1596 response.request_id(),
1597 response.text_opt().unwrap_or("<non-UTF8 body>")
1598 );
1599 }
1600 }};
1601 ($response:expr, $expected:expr, $($msg:tt)+) => {{
1602 let response = &$response;
1603 let actual = response.status_code();
1604 let expected_code: u16 = $crate::testing::IntoStatusU16::into_status_u16($expected);
1605 if actual != expected_code {
1606 panic!(
1607 "{}\n\
1608 assertion failed: `(response.status() == {})`\n\
1609 expected status: {}\n\
1610 actual status: {}\n\
1611 request id: {}\n\
1612 response body: {}",
1613 format_args!($($msg)+),
1614 expected_code,
1615 expected_code,
1616 actual,
1617 response.request_id(),
1618 response.text_opt().unwrap_or("<non-UTF8 body>")
1619 );
1620 }
1621 }};
1622}
1623
1624#[macro_export]
1640macro_rules! assert_header {
1641 ($response:expr, $name:expr, $expected:expr) => {{
1642 let response = &$response;
1643 let name = $name;
1644 let expected = $expected;
1645 let actual = response.header_str(name);
1646 if actual != Some(expected) {
1647 panic!(
1648 "assertion failed: `(response.header(\"{}\") == \"{}\")`\n\
1649 expected header '{}': \"{}\"\n\
1650 actual header '{}': {:?}\n\
1651 request id: {}",
1652 name,
1653 expected,
1654 name,
1655 expected,
1656 name,
1657 actual,
1658 response.request_id()
1659 );
1660 }
1661 }};
1662 ($response:expr, $name:expr, $expected:expr, $($msg:tt)+) => {{
1663 let response = &$response;
1664 let name = $name;
1665 let expected = $expected;
1666 let actual = response.header_str(name);
1667 if actual != Some(expected) {
1668 panic!(
1669 "{}\n\
1670 assertion failed: `(response.header(\"{}\") == \"{}\")`\n\
1671 expected header '{}': \"{}\"\n\
1672 actual header '{}': {:?}\n\
1673 request id: {}",
1674 format_args!($($msg)+),
1675 name,
1676 expected,
1677 name,
1678 expected,
1679 name,
1680 actual,
1681 response.request_id()
1682 );
1683 }
1684 }};
1685}
1686
1687#[macro_export]
1703macro_rules! assert_body_contains {
1704 ($response:expr, $expected:expr) => {{
1705 let response = &$response;
1706 let expected = $expected;
1707 let body = response.text();
1708 if !body.contains(expected) {
1709 panic!(
1710 "assertion failed: response body does not contain \"{}\"\n\
1711 expected substring: \"{}\"\n\
1712 actual body: \"{}\"\n\
1713 request id: {}",
1714 expected, expected, body, response.request_id()
1715 );
1716 }
1717 }};
1718 ($response:expr, $expected:expr, $($msg:tt)+) => {{
1719 let response = &$response;
1720 let expected = $expected;
1721 let body = response.text();
1722 if !body.contains(expected) {
1723 panic!(
1724 "{}\n\
1725 assertion failed: response body does not contain \"{}\"\n\
1726 expected substring: \"{}\"\n\
1727 actual body: \"{}\"\n\
1728 request id: {}",
1729 format_args!($($msg)+),
1730 expected,
1731 expected,
1732 body,
1733 response.request_id()
1734 );
1735 }
1736 }};
1737}
1738
1739#[macro_export]
1766macro_rules! assert_json {
1767 ($response:expr, $expected:tt) => {{
1768 let response = &$response;
1769 let expected = serde_json::json!($expected);
1770 let actual: serde_json::Value = response
1771 .json()
1772 .expect("Failed to parse response body as JSON");
1773
1774 if let Err(path) = $crate::testing::json_contains(&actual, &expected) {
1775 panic!(
1776 "assertion failed: JSON partial match failed at path '{}'\n\
1777 expected (partial):\n{}\n\
1778 actual:\n{}\n\
1779 request id: {}",
1780 path,
1781 serde_json::to_string_pretty(&expected).unwrap_or_else(|_| format!("{:?}", expected)),
1782 serde_json::to_string_pretty(&actual).unwrap_or_else(|_| format!("{:?}", actual)),
1783 response.request_id()
1784 );
1785 }
1786 }};
1787 ($response:expr, $expected:tt, $($msg:tt)+) => {{
1788 let response = &$response;
1789 let expected = serde_json::json!($expected);
1790 let actual: serde_json::Value = response
1791 .json()
1792 .expect("Failed to parse response body as JSON");
1793
1794 if let Err(path) = $crate::testing::json_contains(&actual, &expected) {
1795 panic!(
1796 "{}\n\
1797 assertion failed: JSON partial match failed at path '{}'\n\
1798 expected (partial):\n{}\n\
1799 actual:\n{}\n\
1800 request id: {}",
1801 format_args!($($msg)+),
1802 path,
1803 serde_json::to_string_pretty(&expected).unwrap_or_else(|_| format!("{:?}", expected)),
1804 serde_json::to_string_pretty(&actual).unwrap_or_else(|_| format!("{:?}", actual)),
1805 response.request_id()
1806 );
1807 }
1808 }};
1809}
1810
1811#[cfg(feature = "regex")]
1825#[macro_export]
1826macro_rules! assert_body_matches {
1827 ($response:expr, $pattern:expr) => {{
1828 let response = &$response;
1829 let pattern = $pattern;
1830 let re = regex::Regex::new(pattern)
1831 .unwrap_or_else(|e| panic!("Invalid regex pattern '{}': {}", pattern, e));
1832 let body = response.text();
1833 if !re.is_match(body) {
1834 panic!(
1835 "assertion failed: response body does not match pattern\n\
1836 pattern: \"{}\"\n\
1837 actual body: \"{}\"\n\
1838 request id: {}",
1839 pattern, body, response.request_id()
1840 );
1841 }
1842 }};
1843 ($response:expr, $pattern:expr, $($msg:tt)+) => {{
1844 let response = &$response;
1845 let pattern = $pattern;
1846 let re = regex::Regex::new(pattern)
1847 .unwrap_or_else(|e| panic!("Invalid regex pattern '{}': {}", pattern, e));
1848 let body = response.text();
1849 if !re.is_match(body) {
1850 panic!(
1851 "{}\n\
1852 assertion failed: response body does not match pattern\n\
1853 pattern: \"{}\"\n\
1854 actual body: \"{}\"\n\
1855 request id: {}",
1856 format_args!($($msg)+),
1857 pattern,
1858 body,
1859 response.request_id()
1860 );
1861 }
1862 }};
1863}
1864
1865#[derive(Debug, Clone)]
1890pub struct LabTestConfig {
1891 pub seed: u64,
1893 pub chaos_enabled: bool,
1895 pub chaos_intensity: f64,
1897 pub max_steps: Option<u64>,
1899 pub capture_traces: bool,
1901}
1902
1903impl Default for LabTestConfig {
1904 fn default() -> Self {
1905 Self {
1906 seed: 42,
1907 chaos_enabled: false,
1908 chaos_intensity: 0.0,
1909 max_steps: Some(10_000),
1910 capture_traces: false,
1911 }
1912 }
1913}
1914
1915impl LabTestConfig {
1916 #[must_use]
1918 pub fn new(seed: u64) -> Self {
1919 Self {
1920 seed,
1921 ..Default::default()
1922 }
1923 }
1924
1925 #[must_use]
1929 pub fn with_light_chaos(mut self) -> Self {
1930 self.chaos_enabled = true;
1931 self.chaos_intensity = 0.05;
1932 self
1933 }
1934
1935 #[must_use]
1939 pub fn with_heavy_chaos(mut self) -> Self {
1940 self.chaos_enabled = true;
1941 self.chaos_intensity = 0.2;
1942 self
1943 }
1944
1945 #[must_use]
1947 pub fn with_chaos_intensity(mut self, intensity: f64) -> Self {
1948 self.chaos_enabled = intensity > 0.0;
1949 self.chaos_intensity = intensity.clamp(0.0, 1.0);
1950 self
1951 }
1952
1953 #[must_use]
1955 pub fn with_max_steps(mut self, max: u64) -> Self {
1956 self.max_steps = Some(max);
1957 self
1958 }
1959
1960 #[must_use]
1962 pub fn without_step_limit(mut self) -> Self {
1963 self.max_steps = None;
1964 self
1965 }
1966
1967 #[must_use]
1969 pub fn with_traces(mut self) -> Self {
1970 self.capture_traces = true;
1971 self
1972 }
1973}
1974
1975#[derive(Debug, Clone, Default)]
1979pub struct TestChaosStats {
1980 pub decision_points: u64,
1982 pub delays_injected: u64,
1984 pub cancellations_injected: u64,
1986 pub steps_executed: u64,
1988}
1989
1990impl TestChaosStats {
1991 #[must_use]
1993 #[allow(clippy::cast_precision_loss)] pub fn injection_rate(&self) -> f64 {
1995 if self.decision_points == 0 {
1996 0.0
1997 } else {
1998 (self.delays_injected + self.cancellations_injected) as f64
1999 / self.decision_points as f64
2000 }
2001 }
2002
2003 #[must_use]
2005 pub fn had_chaos(&self) -> bool {
2006 self.delays_injected > 0 || self.cancellations_injected > 0
2007 }
2008}
2009
2010#[derive(Debug, Clone)]
2029pub struct MockTime {
2030 current_us: Arc<std::sync::atomic::AtomicU64>,
2032}
2033
2034impl Default for MockTime {
2035 fn default() -> Self {
2036 Self::new()
2037 }
2038}
2039
2040impl MockTime {
2041 #[must_use]
2043 pub fn new() -> Self {
2044 Self {
2045 current_us: Arc::new(std::sync::atomic::AtomicU64::new(0)),
2046 }
2047 }
2048
2049 #[must_use]
2051 pub fn starting_at(initial: std::time::Duration) -> Self {
2052 Self {
2053 current_us: Arc::new(std::sync::atomic::AtomicU64::new(initial.as_micros() as u64)),
2054 }
2055 }
2056
2057 #[must_use]
2059 pub fn now(&self) -> std::time::Duration {
2060 std::time::Duration::from_micros(self.current_us.load(std::sync::atomic::Ordering::Relaxed))
2061 }
2062
2063 #[must_use]
2065 pub fn elapsed(&self) -> std::time::Duration {
2066 self.now()
2067 }
2068
2069 pub fn advance(&self, duration: std::time::Duration) {
2071 self.current_us.fetch_add(
2072 duration.as_micros() as u64,
2073 std::sync::atomic::Ordering::Relaxed,
2074 );
2075 }
2076
2077 pub fn set(&self, time: std::time::Duration) {
2079 self.current_us.store(
2080 time.as_micros() as u64,
2081 std::sync::atomic::Ordering::Relaxed,
2082 );
2083 }
2084
2085 pub fn reset(&self) {
2087 self.current_us
2088 .store(0, std::sync::atomic::Ordering::Relaxed);
2089 }
2090}
2091
2092#[derive(Debug)]
2096pub struct CancellationTestResult {
2097 pub completed: bool,
2099 pub cancelled_at_checkpoint: bool,
2101 pub response: Option<Response>,
2103 pub cancellation_point: Option<String>,
2105}
2106
2107impl CancellationTestResult {
2108 #[must_use]
2110 pub fn gracefully_cancelled(&self) -> bool {
2111 !self.completed && self.cancelled_at_checkpoint
2112 }
2113
2114 #[must_use]
2116 pub fn completed_despite_cancel(&self) -> bool {
2117 self.completed
2118 }
2119}
2120
2121pub struct CancellationTest<H> {
2135 handler: H,
2136 seed: u64,
2137}
2138
2139impl<H: Handler + 'static> CancellationTest<H> {
2140 #[must_use]
2142 pub fn new(handler: H) -> Self {
2143 Self { handler, seed: 42 }
2144 }
2145
2146 #[must_use]
2148 pub fn with_seed(mut self, seed: u64) -> Self {
2149 self.seed = seed;
2150 self
2151 }
2152
2153 pub fn test_respects_cancellation(&self) -> CancellationTestResult {
2159 let cx = asupersync::Cx::for_testing();
2160 let ctx = RequestContext::new(cx, 1);
2161
2162 ctx.cx().set_cancel_requested(true);
2164
2165 let mut request = Request::new(Method::Get, "/test");
2166 let response = futures_executor::block_on(self.handler.call(&ctx, &mut request));
2167
2168 let is_cancelled_response = response.status().as_u16() == 499
2170 || response.status().as_u16() == 504
2171 || response.status().as_u16() == 503;
2172
2173 CancellationTestResult {
2174 completed: true,
2175 cancelled_at_checkpoint: is_cancelled_response,
2176 response: Some(response),
2177 cancellation_point: None,
2178 }
2179 }
2180
2181 pub fn complete_normally(&self) -> CancellationTestResult {
2183 let cx = asupersync::Cx::for_testing();
2184 let ctx = RequestContext::new(cx, 1);
2185 let mut request = Request::new(Method::Get, "/test");
2186
2187 let response = futures_executor::block_on(self.handler.call(&ctx, &mut request));
2188
2189 CancellationTestResult {
2190 completed: true,
2191 cancelled_at_checkpoint: false,
2192 response: Some(response),
2193 cancellation_point: None,
2194 }
2195 }
2196
2197 pub fn test_with_cancel_callback<F>(
2202 &self,
2203 path: &str,
2204 mut cancel_fn: F,
2205 ) -> CancellationTestResult
2206 where
2207 F: FnMut(&RequestContext) -> bool,
2208 {
2209 let cx = asupersync::Cx::for_testing();
2210 let ctx = RequestContext::new(cx, 1);
2211
2212 if cancel_fn(&ctx) {
2214 ctx.cx().set_cancel_requested(true);
2215 }
2216
2217 let mut request = Request::new(Method::Get, path);
2218 let response = futures_executor::block_on(self.handler.call(&ctx, &mut request));
2219
2220 let is_cancelled = ctx.is_cancelled();
2221 let is_cancelled_response =
2222 response.status().as_u16() == 499 || response.status().as_u16() == 504;
2223
2224 CancellationTestResult {
2225 completed: true,
2226 cancelled_at_checkpoint: is_cancelled && is_cancelled_response,
2227 response: Some(response),
2228 cancellation_point: None,
2229 }
2230 }
2231}
2232
2233#[cfg(test)]
2234mod tests {
2235 use super::*;
2236 use crate::app::App;
2237 use crate::dependency::{Depends, FromDependency};
2238 use crate::error::HttpError;
2239 use crate::extract::FromRequest;
2240 use crate::middleware::BoxFuture;
2241
2242 struct EchoHandler;
2244
2245 impl Handler for EchoHandler {
2246 fn call<'a>(
2247 &'a self,
2248 _ctx: &'a RequestContext,
2249 req: &'a mut Request,
2250 ) -> BoxFuture<'a, Response> {
2251 let method = format!("{:?}", req.method());
2252 let path = req.path().to_string();
2253 let body = format!("Method: {method}, Path: {path}");
2254 Box::pin(async move {
2255 Response::ok()
2256 .header("content-type", b"text/plain".to_vec())
2257 .body(ResponseBody::Bytes(body.into_bytes()))
2258 })
2259 }
2260 }
2261
2262 struct CookieHandler;
2264
2265 impl Handler for CookieHandler {
2266 fn call<'a>(
2267 &'a self,
2268 _ctx: &'a RequestContext,
2269 _req: &'a mut Request,
2270 ) -> BoxFuture<'a, Response> {
2271 Box::pin(async move {
2272 Response::ok()
2273 .header("set-cookie", b"session=abc123".to_vec())
2274 .body(ResponseBody::Bytes(b"Cookie set".to_vec()))
2275 })
2276 }
2277 }
2278
2279 struct CookieEchoHandler;
2281
2282 impl Handler for CookieEchoHandler {
2283 fn call<'a>(
2284 &'a self,
2285 _ctx: &'a RequestContext,
2286 req: &'a mut Request,
2287 ) -> BoxFuture<'a, Response> {
2288 let cookie = req.headers().get("cookie").map_or_else(
2289 || "no cookies".to_string(),
2290 |v| String::from_utf8_lossy(v).to_string(),
2291 );
2292 Box::pin(async move { Response::ok().body(ResponseBody::Bytes(cookie.into_bytes())) })
2293 }
2294 }
2295
2296 #[derive(Clone)]
2297 struct OverrideDep {
2298 value: usize,
2299 }
2300
2301 impl FromDependency for OverrideDep {
2302 type Error = HttpError;
2303
2304 async fn from_dependency(
2305 _ctx: &RequestContext,
2306 _req: &mut Request,
2307 ) -> Result<Self, Self::Error> {
2308 Ok(Self { value: 1 })
2309 }
2310 }
2311
2312 struct OverrideDepHandler;
2313
2314 impl Handler for OverrideDepHandler {
2315 fn call<'a>(
2316 &'a self,
2317 ctx: &'a RequestContext,
2318 req: &'a mut Request,
2319 ) -> BoxFuture<'a, Response> {
2320 Box::pin(async move {
2321 let dep = Depends::<OverrideDep>::from_request(ctx, req)
2322 .await
2323 .expect("dependency extraction failed");
2324 Response::ok().body(ResponseBody::Bytes(dep.value.to_string().into_bytes()))
2325 })
2326 }
2327 }
2328
2329 fn override_dep_route(ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
2330 let dep = futures_executor::block_on(Depends::<OverrideDep>::from_request(ctx, req))
2331 .expect("dependency extraction failed");
2332 std::future::ready(
2333 Response::ok().body(ResponseBody::Bytes(dep.value.to_string().into_bytes())),
2334 )
2335 }
2336
2337 #[test]
2338 fn test_client_get() {
2339 let client = TestClient::new(EchoHandler);
2340 let response = client.get("/test/path").send();
2341
2342 assert_eq!(response.status_code(), 200);
2343 assert_eq!(response.text(), "Method: Get, Path: /test/path");
2344 }
2345
2346 #[test]
2347 fn test_client_post() {
2348 let client = TestClient::new(EchoHandler);
2349 let response = client.post("/api/items").send();
2350
2351 assert_eq!(response.status_code(), 200);
2352 assert!(response.text().contains("Method: Post"));
2353 }
2354
2355 #[test]
2360 #[ignore = "nested executor issue: override_dep_route uses block_on inside TestClient's block_on"]
2361 fn test_app_dependency_override_used_by_test_client() {
2362 let app = App::builder()
2363 .route("/", Method::Get, override_dep_route)
2364 .build();
2365
2366 app.override_dependency_value(OverrideDep { value: 42 });
2367
2368 let client = TestClient::new(app);
2369
2370 let response = client.get("/").send();
2371
2372 assert_eq!(response.text(), "42");
2373 }
2374
2375 #[test]
2376 fn test_test_client_override_clear() {
2377 let client = TestClient::new(OverrideDepHandler);
2378
2379 client.override_dependency_value(OverrideDep { value: 9 });
2380 let response = client.get("/").send();
2381 assert_eq!(response.text(), "9");
2382
2383 client.clear_dependency_overrides();
2384 let response = client.get("/").send();
2385 assert_eq!(response.text(), "1");
2386 }
2387
2388 #[test]
2389 fn test_client_all_methods() {
2390 let client = TestClient::new(EchoHandler);
2391
2392 assert!(client.get("/").send().text().contains("Get"));
2393 assert!(client.post("/").send().text().contains("Post"));
2394 assert!(client.put("/").send().text().contains("Put"));
2395 assert!(client.delete("/").send().text().contains("Delete"));
2396 assert!(client.patch("/").send().text().contains("Patch"));
2397 assert!(client.options("/").send().text().contains("Options"));
2398 assert!(client.head("/").send().text().contains("Head"));
2399 }
2400
2401 #[test]
2402 fn test_query_params() {
2403 let client = TestClient::new(EchoHandler);
2404 let response = client
2405 .get("/search")
2406 .query("q", "rust")
2407 .query("limit", "10")
2408 .send();
2409
2410 assert_eq!(response.status_code(), 200);
2411 }
2412
2413 #[test]
2414 fn test_response_assertions() {
2415 let client = TestClient::new(EchoHandler);
2416 let response = client.get("/test").send();
2417
2418 let _ = response
2419 .assert_status_code(200)
2420 .assert_success()
2421 .assert_header("content-type", "text/plain")
2422 .assert_text_contains("Get");
2423 }
2424
2425 #[test]
2426 fn test_response_status_checks() {
2427 let client = TestClient::new(EchoHandler);
2428 let response = client.get("/").send();
2429
2430 assert!(response.is_success());
2431 assert!(!response.is_redirect());
2432 assert!(!response.is_client_error());
2433 assert!(!response.is_server_error());
2434 }
2435
2436 #[test]
2437 fn test_cookie_jar() {
2438 let mut jar = CookieJar::new();
2439 assert!(jar.is_empty());
2440
2441 jar.set("session", "abc123");
2442 jar.set("user", "alice");
2443
2444 assert_eq!(jar.len(), 2);
2445 assert_eq!(jar.get("session"), Some("abc123"));
2446 assert_eq!(jar.get("user"), Some("alice"));
2447
2448 let header = jar.to_cookie_header().unwrap();
2449 assert!(header.contains("session=abc123"));
2450 assert!(header.contains("user=alice"));
2451
2452 jar.remove("session");
2453 assert_eq!(jar.len(), 1);
2454 assert_eq!(jar.get("session"), None);
2455 }
2456
2457 #[test]
2458 fn test_cookie_jar_request_matching_rules() {
2459 use crate::request::ConnectionInfo;
2460
2461 let mut jar = CookieJar::new();
2462
2463 let mut req = Request::new(Method::Get, "/account/settings");
2464 req.headers_mut().insert("host", b"example.com".to_vec());
2465
2466 jar.parse_set_cookie(
2468 &req,
2469 b"sid=1; Path=/account; Secure; HttpOnly; SameSite=Lax",
2470 );
2471 assert_eq!(jar.cookie_header_for_request(&req), None);
2472
2473 req.insert_extension(ConnectionInfo::HTTPS);
2475 assert_eq!(
2476 jar.cookie_header_for_request(&req).as_deref(),
2477 Some("sid=1")
2478 );
2479
2480 let mut req2 = Request::new(Method::Get, "/other");
2482 req2.headers_mut().insert("host", b"example.com".to_vec());
2483 req2.insert_extension(ConnectionInfo::HTTPS);
2484 assert_eq!(jar.cookie_header_for_request(&req2), None);
2485
2486 jar.parse_set_cookie(&req, b"sub=1; Domain=example.com; Path=/");
2488 let mut req3 = Request::new(Method::Get, "/");
2489 req3.headers_mut()
2490 .insert("host", b"api.example.com".to_vec());
2491 let hdr = jar.cookie_header_for_request(&req3).expect("cookie header");
2492 assert!(hdr.contains("sub=1"));
2493 }
2494
2495 #[test]
2496 fn test_cookie_persistence() {
2497 let client = TestClient::new(CookieHandler);
2498
2499 let _ = client.get("/set-cookie").send();
2501
2502 assert_eq!(client.cookies().get("session"), Some("abc123"));
2504
2505 let client2 = TestClient::new(CookieEchoHandler);
2507 client2.cookies().set("session", "abc123");
2508
2509 let response = client2.get("/check-cookie").send();
2510 assert!(response.text().contains("session=abc123"));
2511 }
2512
2513 #[test]
2514 fn test_request_id_increments() {
2515 let client = TestClient::new(EchoHandler);
2516
2517 let r1 = client.get("/").send();
2518 let r2 = client.get("/").send();
2519 let r3 = client.get("/").send();
2520
2521 assert_eq!(r1.request_id(), 1);
2522 assert_eq!(r2.request_id(), 2);
2523 assert_eq!(r3.request_id(), 3);
2524 }
2525
2526 #[test]
2527 fn test_client_with_seed() {
2528 let client = TestClient::with_seed(EchoHandler, 42);
2529 assert_eq!(client.seed(), Some(42));
2530 }
2531
2532 #[test]
2533 fn test_client_clone() {
2534 let client = TestClient::new(EchoHandler);
2535 client.cookies().set("test", "value");
2536
2537 let cloned = client.clone();
2538
2539 assert_eq!(cloned.cookies().get("test"), Some("value"));
2541
2542 let r1 = client.get("/").send();
2544 let r2 = cloned.get("/").send();
2545 assert_eq!(r1.request_id(), 1);
2546 assert_eq!(r2.request_id(), 2);
2547 }
2548
2549 #[test]
2554 fn test_json_contains_exact_match() {
2555 let actual = serde_json::json!({"id": 1, "name": "Alice"});
2556 let expected = serde_json::json!({"id": 1, "name": "Alice"});
2557 assert!(json_contains(&actual, &expected).is_ok());
2558 }
2559
2560 #[test]
2561 fn test_json_contains_partial_match() {
2562 let actual = serde_json::json!({"id": 1, "name": "Alice", "email": "alice@example.com"});
2563 let expected = serde_json::json!({"name": "Alice"});
2564 assert!(json_contains(&actual, &expected).is_ok());
2565 }
2566
2567 #[test]
2568 fn test_json_contains_nested_partial_match() {
2569 let actual = serde_json::json!({
2570 "user": {"id": 1, "name": "Alice", "email": "alice@example.com"},
2571 "status": "active"
2572 });
2573 let expected = serde_json::json!({
2574 "user": {"name": "Alice"}
2575 });
2576 assert!(json_contains(&actual, &expected).is_ok());
2577 }
2578
2579 #[test]
2580 fn test_json_contains_mismatch_value() {
2581 let actual = serde_json::json!({"id": 1, "name": "Alice"});
2582 let expected = serde_json::json!({"name": "Bob"});
2583 let result = json_contains(&actual, &expected);
2584 assert!(result.is_err());
2585 assert_eq!(result.unwrap_err(), "$.name");
2586 }
2587
2588 #[test]
2589 fn test_json_contains_missing_key() {
2590 let actual = serde_json::json!({"id": 1, "name": "Alice"});
2591 let expected = serde_json::json!({"email": "alice@example.com"});
2592 let result = json_contains(&actual, &expected);
2593 assert!(result.is_err());
2594 assert_eq!(result.unwrap_err(), "$.email");
2595 }
2596
2597 #[test]
2598 fn test_json_contains_array_exact_match() {
2599 let actual = serde_json::json!({"items": [1, 2, 3]});
2600 let expected = serde_json::json!({"items": [1, 2, 3]});
2601 assert!(json_contains(&actual, &expected).is_ok());
2602 }
2603
2604 #[test]
2605 fn test_json_contains_array_length_mismatch() {
2606 let actual = serde_json::json!({"items": [1, 2, 3]});
2607 let expected = serde_json::json!({"items": [1, 2]});
2608 let result = json_contains(&actual, &expected);
2609 assert!(result.is_err());
2610 assert_eq!(result.unwrap_err(), "$.items[length]");
2611 }
2612
2613 #[test]
2614 fn test_json_contains_array_element_mismatch() {
2615 let actual = serde_json::json!({"items": [1, 2, 3]});
2616 let expected = serde_json::json!({"items": [1, 5, 3]});
2617 let result = json_contains(&actual, &expected);
2618 assert!(result.is_err());
2619 assert_eq!(result.unwrap_err(), "$.items[1]");
2620 }
2621
2622 #[test]
2623 fn test_json_contains_primitives() {
2624 assert!(json_contains(&serde_json::json!(42), &serde_json::json!(42)).is_ok());
2626 assert!(json_contains(&serde_json::json!(42), &serde_json::json!(43)).is_err());
2627
2628 assert!(json_contains(&serde_json::json!("hello"), &serde_json::json!("hello")).is_ok());
2630 assert!(json_contains(&serde_json::json!("hello"), &serde_json::json!("world")).is_err());
2631
2632 assert!(json_contains(&serde_json::json!(true), &serde_json::json!(true)).is_ok());
2634 assert!(json_contains(&serde_json::json!(true), &serde_json::json!(false)).is_err());
2635
2636 assert!(json_contains(&serde_json::json!(null), &serde_json::json!(null)).is_ok());
2638 }
2639
2640 #[test]
2641 fn test_json_contains_type_mismatch() {
2642 let actual = serde_json::json!({"id": "1"});
2643 let expected = serde_json::json!({"id": 1});
2644 let result = json_contains(&actual, &expected);
2645 assert!(result.is_err());
2646 assert_eq!(result.unwrap_err(), "$.id");
2647 }
2648
2649 #[test]
2650 fn test_json_contains_deeply_nested() {
2651 let actual = serde_json::json!({
2652 "level1": {
2653 "level2": {
2654 "level3": {
2655 "value": 42,
2656 "extra": "ignored"
2657 }
2658 }
2659 }
2660 });
2661 let expected = serde_json::json!({
2662 "level1": {
2663 "level2": {
2664 "level3": {
2665 "value": 42
2666 }
2667 }
2668 }
2669 });
2670 assert!(json_contains(&actual, &expected).is_ok());
2671 }
2672
2673 struct JsonHandler;
2679
2680 impl Handler for JsonHandler {
2681 fn call<'a>(
2682 &'a self,
2683 _ctx: &'a RequestContext,
2684 _req: &'a mut Request,
2685 ) -> BoxFuture<'a, Response> {
2686 let json = serde_json::json!({
2687 "id": 1,
2688 "name": "Alice",
2689 "email": "alice@example.com",
2690 "active": true
2691 });
2692 let body = serde_json::to_vec(&json).unwrap();
2693 Box::pin(async move {
2694 Response::ok()
2695 .header("content-type", b"application/json".to_vec())
2696 .header("x-request-id", b"req-123".to_vec())
2697 .body(ResponseBody::Bytes(body))
2698 })
2699 }
2700 }
2701
2702 #[allow(dead_code)]
2704 struct StatusHandler(u16);
2705
2706 #[allow(dead_code)]
2707 impl Handler for StatusHandler {
2708 fn call<'a>(
2709 &'a self,
2710 _ctx: &'a RequestContext,
2711 _req: &'a mut Request,
2712 ) -> BoxFuture<'a, Response> {
2713 let status = StatusCode::from_u16(self.0);
2714 Box::pin(async move { Response::with_status(status) })
2715 }
2716 }
2717
2718 #[test]
2719 fn test_assert_status_macro_with_u16() {
2720 let client = TestClient::new(EchoHandler);
2721 let response = client.get("/").send();
2722 crate::assert_status!(response, 200);
2723 }
2724
2725 #[test]
2726 fn test_assert_status_macro_with_status_code() {
2727 let client = TestClient::new(EchoHandler);
2728 let response = client.get("/").send();
2729 crate::assert_status!(response, StatusCode::OK);
2730 }
2731
2732 #[test]
2733 #[should_panic(expected = "assertion failed")]
2734 fn test_assert_status_macro_failure() {
2735 let client = TestClient::new(EchoHandler);
2736 let response = client.get("/").send();
2737 crate::assert_status!(response, 404);
2738 }
2739
2740 #[test]
2741 fn test_assert_header_macro() {
2742 let client = TestClient::new(EchoHandler);
2743 let response = client.get("/").send();
2744 crate::assert_header!(response, "content-type", "text/plain");
2745 }
2746
2747 #[test]
2748 #[should_panic(expected = "assertion failed")]
2749 fn test_assert_header_macro_failure() {
2750 let client = TestClient::new(EchoHandler);
2751 let response = client.get("/").send();
2752 crate::assert_header!(response, "content-type", "application/json");
2753 }
2754
2755 #[test]
2756 fn test_assert_body_contains_macro() {
2757 let client = TestClient::new(EchoHandler);
2758 let response = client.get("/test").send();
2759 crate::assert_body_contains!(response, "Method: Get");
2760 crate::assert_body_contains!(response, "Path: /test");
2761 }
2762
2763 #[test]
2764 #[should_panic(expected = "assertion failed")]
2765 fn test_assert_body_contains_macro_failure() {
2766 let client = TestClient::new(EchoHandler);
2767 let response = client.get("/test").send();
2768 crate::assert_body_contains!(response, "nonexistent");
2769 }
2770
2771 #[test]
2772 fn test_assert_json_macro_partial_match() {
2773 let client = TestClient::new(JsonHandler);
2774 let response = client.get("/user").send();
2775
2776 crate::assert_json!(response, {"name": "Alice"});
2778 crate::assert_json!(response, {"id": 1, "active": true});
2779 }
2780
2781 #[test]
2782 fn test_assert_json_macro_exact_match() {
2783 let client = TestClient::new(JsonHandler);
2784 let response = client.get("/user").send();
2785
2786 crate::assert_json!(response, {
2788 "id": 1,
2789 "name": "Alice",
2790 "email": "alice@example.com",
2791 "active": true
2792 });
2793 }
2794
2795 #[test]
2796 #[should_panic(expected = "JSON partial match failed")]
2797 fn test_assert_json_macro_failure() {
2798 let client = TestClient::new(JsonHandler);
2799 let response = client.get("/user").send();
2800 crate::assert_json!(response, {"name": "Bob"});
2801 }
2802
2803 #[test]
2808 fn test_assert_json_contains_method() {
2809 let client = TestClient::new(JsonHandler);
2810 let response = client.get("/user").send();
2811
2812 let _ = response.assert_json_contains(&serde_json::json!({"name": "Alice"}));
2813 }
2814
2815 #[test]
2816 fn test_assert_header_exists() {
2817 let client = TestClient::new(JsonHandler);
2818 let response = client.get("/").send();
2819
2820 let _ = response
2821 .assert_header_exists("content-type")
2822 .assert_header_exists("x-request-id");
2823 }
2824
2825 #[test]
2826 #[should_panic(expected = "Expected header 'nonexistent' to exist")]
2827 fn test_assert_header_exists_failure() {
2828 let client = TestClient::new(JsonHandler);
2829 let response = client.get("/").send();
2830 let _ = response.assert_header_exists("nonexistent");
2831 }
2832
2833 #[test]
2834 fn test_assert_header_missing() {
2835 let client = TestClient::new(JsonHandler);
2836 let response = client.get("/").send();
2837
2838 let _ = response.assert_header_missing("x-nonexistent");
2839 }
2840
2841 #[test]
2842 #[should_panic(expected = "Expected header 'content-type' to not exist")]
2843 fn test_assert_header_missing_failure() {
2844 let client = TestClient::new(JsonHandler);
2845 let response = client.get("/").send();
2846 let _ = response.assert_header_missing("content-type");
2847 }
2848
2849 #[test]
2850 fn test_assert_content_type_contains() {
2851 let client = TestClient::new(JsonHandler);
2852 let response = client.get("/").send();
2853
2854 let _ = response.assert_content_type_contains("application/json");
2855 let _ = response.assert_content_type_contains("json");
2856 }
2857
2858 #[test]
2859 #[should_panic(expected = "Expected Content-Type to contain")]
2860 fn test_assert_content_type_contains_failure() {
2861 let client = TestClient::new(JsonHandler);
2862 let response = client.get("/").send();
2863 let _ = response.assert_content_type_contains("text/html");
2864 }
2865
2866 #[test]
2867 fn test_assertion_chaining() {
2868 let client = TestClient::new(JsonHandler);
2869 let response = client.get("/user").send();
2870
2871 let _ = response
2873 .assert_status_code(200)
2874 .assert_success()
2875 .assert_header_exists("content-type")
2876 .assert_content_type_contains("json")
2877 .assert_json_contains(&serde_json::json!({"name": "Alice"}));
2878 }
2879
2880 #[test]
2881 fn test_macro_with_custom_message() {
2882 let client = TestClient::new(EchoHandler);
2883 let response = client.get("/").send();
2884
2885 crate::assert_status!(response, 200, "Expected 200 OK from echo handler");
2887 crate::assert_header!(
2888 response,
2889 "content-type",
2890 "text/plain",
2891 "Should have text content type"
2892 );
2893 crate::assert_body_contains!(response, "Get", "Should contain HTTP method");
2894 }
2895
2896 #[derive(Clone)]
2902 struct DatabasePool {
2903 connection_string: String,
2904 }
2905
2906 impl FromDependency for DatabasePool {
2907 type Error = HttpError;
2908 async fn from_dependency(
2909 _ctx: &RequestContext,
2910 _req: &mut Request,
2911 ) -> Result<Self, Self::Error> {
2912 Ok(DatabasePool {
2913 connection_string: "postgres://localhost/test".to_string(),
2914 })
2915 }
2916 }
2917
2918 #[derive(Clone)]
2919 struct UserRepository {
2920 pool_conn_str: String,
2921 }
2922
2923 impl FromDependency for UserRepository {
2924 type Error = HttpError;
2925 async fn from_dependency(
2926 ctx: &RequestContext,
2927 req: &mut Request,
2928 ) -> Result<Self, Self::Error> {
2929 let pool = Depends::<DatabasePool>::from_request(ctx, req).await?;
2930 Ok(UserRepository {
2931 pool_conn_str: pool.connection_string.clone(),
2932 })
2933 }
2934 }
2935
2936 #[derive(Clone)]
2937 struct AuthService {
2938 user_repo_pool: String,
2939 }
2940
2941 impl FromDependency for AuthService {
2942 type Error = HttpError;
2943 async fn from_dependency(
2944 ctx: &RequestContext,
2945 req: &mut Request,
2946 ) -> Result<Self, Self::Error> {
2947 let repo = Depends::<UserRepository>::from_request(ctx, req).await?;
2948 Ok(AuthService {
2949 user_repo_pool: repo.pool_conn_str.clone(),
2950 })
2951 }
2952 }
2953
2954 struct ComplexDepHandler;
2955
2956 impl Handler for ComplexDepHandler {
2957 fn call<'a>(
2958 &'a self,
2959 ctx: &'a RequestContext,
2960 req: &'a mut Request,
2961 ) -> BoxFuture<'a, Response> {
2962 Box::pin(async move {
2963 let auth = Depends::<AuthService>::from_request(ctx, req)
2964 .await
2965 .expect("dependency resolution failed");
2966 let body = format!("AuthService.pool={}", auth.user_repo_pool);
2967 Response::ok().body(ResponseBody::Bytes(body.into_bytes()))
2968 })
2969 }
2970 }
2971
2972 #[test]
2973 fn test_full_request_with_complex_deps() {
2974 let client = TestClient::new(ComplexDepHandler);
2977 let response = client.get("/auth/check").send();
2978
2979 assert_eq!(response.status_code(), 200);
2980 assert!(response.text().contains("postgres://localhost/test"));
2981 }
2982
2983 #[test]
2984 fn test_complex_deps_with_override_at_leaf() {
2985 let client = TestClient::new(ComplexDepHandler);
2987 client.override_dependency_value(DatabasePool {
2988 connection_string: "mysql://prod/users".to_string(),
2989 });
2990
2991 let response = client.get("/auth/check").send();
2992
2993 assert_eq!(response.status_code(), 200);
2994 assert!(
2995 response.text().contains("mysql://prod/users"),
2996 "Override at leaf should propagate through dependency chain"
2997 );
2998 }
2999
3000 #[test]
3001 fn test_complex_deps_with_override_at_middle() {
3002 let client = TestClient::new(ComplexDepHandler);
3004 client.override_dependency_value(UserRepository {
3005 pool_conn_str: "overridden-repo-connection".to_string(),
3006 });
3007
3008 let response = client.get("/auth/check").send();
3009
3010 assert_eq!(response.status_code(), 200);
3011 assert!(
3012 response.text().contains("overridden-repo-connection"),
3013 "Override at middle level should be used"
3014 );
3015 }
3016
3017 #[test]
3018 fn test_dependency_caching_across_handler() {
3019 use std::sync::atomic::{AtomicUsize, Ordering};
3021
3022 static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
3023
3024 #[derive(Clone)]
3025 struct TrackedDep {
3026 call_number: usize,
3027 }
3028
3029 impl FromDependency for TrackedDep {
3030 type Error = HttpError;
3031 async fn from_dependency(
3032 _ctx: &RequestContext,
3033 _req: &mut Request,
3034 ) -> Result<Self, Self::Error> {
3035 let call_number = CALL_COUNT.fetch_add(1, Ordering::SeqCst);
3036 Ok(TrackedDep { call_number })
3037 }
3038 }
3039
3040 struct MultiDepHandler;
3041
3042 impl Handler for MultiDepHandler {
3043 fn call<'a>(
3044 &'a self,
3045 ctx: &'a RequestContext,
3046 req: &'a mut Request,
3047 ) -> BoxFuture<'a, Response> {
3048 Box::pin(async move {
3049 let dep1 = Depends::<TrackedDep>::from_request(ctx, req)
3051 .await
3052 .expect("first resolution failed");
3053 let dep2 = Depends::<TrackedDep>::from_request(ctx, req)
3054 .await
3055 .expect("second resolution failed");
3056
3057 let body = format!("dep1={} dep2={}", dep1.call_number, dep2.call_number);
3059 Response::ok().body(ResponseBody::Bytes(body.into_bytes()))
3060 })
3061 }
3062 }
3063
3064 CALL_COUNT.store(0, Ordering::SeqCst);
3066
3067 let client = TestClient::new(MultiDepHandler);
3068 let response = client.get("/").send();
3069
3070 let text = response.text();
3071 assert!(
3073 text.contains("dep1=0 dep2=0"),
3074 "Dependencies should be cached within request. Got: {}",
3075 text
3076 );
3077
3078 assert_eq!(CALL_COUNT.load(Ordering::SeqCst), 1);
3080 }
3081
3082 #[test]
3087 #[allow(clippy::float_cmp)] fn lab_test_config_defaults() {
3089 let config = LabTestConfig::default();
3090 assert_eq!(config.seed, 42);
3091 assert!(!config.chaos_enabled);
3092 assert_eq!(config.chaos_intensity, 0.0);
3093 assert_eq!(config.max_steps, Some(10_000));
3094 assert!(!config.capture_traces);
3095 }
3096
3097 #[test]
3098 fn lab_test_config_with_seed() {
3099 let config = LabTestConfig::new(12345);
3100 assert_eq!(config.seed, 12345);
3101 }
3102
3103 #[test]
3104 #[allow(clippy::float_cmp)] fn lab_test_config_light_chaos() {
3106 let config = LabTestConfig::new(42).with_light_chaos();
3107 assert!(config.chaos_enabled);
3108 assert_eq!(config.chaos_intensity, 0.05);
3109 }
3110
3111 #[test]
3112 #[allow(clippy::float_cmp)] fn lab_test_config_heavy_chaos() {
3114 let config = LabTestConfig::new(42).with_heavy_chaos();
3115 assert!(config.chaos_enabled);
3116 assert_eq!(config.chaos_intensity, 0.2);
3117 }
3118
3119 #[test]
3120 #[allow(clippy::float_cmp)] fn lab_test_config_custom_intensity() {
3122 let config = LabTestConfig::new(42).with_chaos_intensity(0.15);
3123 assert!(config.chaos_enabled);
3124 assert_eq!(config.chaos_intensity, 0.15);
3125 }
3126
3127 #[test]
3128 #[allow(clippy::float_cmp)] fn lab_test_config_intensity_clamps() {
3130 let config = LabTestConfig::new(42).with_chaos_intensity(1.5);
3131 assert_eq!(config.chaos_intensity, 1.0);
3132
3133 let config = LabTestConfig::new(42).with_chaos_intensity(-0.5);
3134 assert_eq!(config.chaos_intensity, 0.0);
3135 assert!(!config.chaos_enabled);
3136 }
3137
3138 #[test]
3139 fn lab_test_config_max_steps() {
3140 let config = LabTestConfig::new(42).with_max_steps(1000);
3141 assert_eq!(config.max_steps, Some(1000));
3142 }
3143
3144 #[test]
3145 fn lab_test_config_no_step_limit() {
3146 let config = LabTestConfig::new(42).without_step_limit();
3147 assert_eq!(config.max_steps, None);
3148 }
3149
3150 #[test]
3151 fn lab_test_config_with_traces() {
3152 let config = LabTestConfig::new(42).with_traces();
3153 assert!(config.capture_traces);
3154 }
3155
3156 #[test]
3157 #[allow(clippy::float_cmp)] fn test_chaos_stats_empty() {
3159 let stats = TestChaosStats::default();
3160 assert_eq!(stats.decision_points, 0);
3161 assert_eq!(stats.delays_injected, 0);
3162 assert_eq!(stats.cancellations_injected, 0);
3163 assert_eq!(stats.injection_rate(), 0.0);
3164 assert!(!stats.had_chaos());
3165 }
3166
3167 #[test]
3168 fn test_chaos_stats_with_injections() {
3169 let stats = TestChaosStats {
3170 decision_points: 100,
3171 delays_injected: 5,
3172 cancellations_injected: 2,
3173 steps_executed: 50,
3174 };
3175 assert!((stats.injection_rate() - 0.07).abs() < 0.001);
3176 assert!(stats.had_chaos());
3177 }
3178
3179 #[test]
3180 fn mock_time_basic() {
3181 let time = MockTime::new();
3182 assert_eq!(time.now(), std::time::Duration::ZERO);
3183 assert_eq!(time.elapsed(), std::time::Duration::ZERO);
3184
3185 time.advance(std::time::Duration::from_secs(5));
3186 assert_eq!(time.now(), std::time::Duration::from_secs(5));
3187 }
3188
3189 #[test]
3190 fn mock_time_set_and_reset() {
3191 let time = MockTime::new();
3192 time.set(std::time::Duration::from_secs(100));
3193 assert_eq!(time.now(), std::time::Duration::from_secs(100));
3194
3195 time.reset();
3196 assert_eq!(time.now(), std::time::Duration::ZERO);
3197 }
3198
3199 #[test]
3200 fn mock_time_starting_at() {
3201 let time = MockTime::starting_at(std::time::Duration::from_secs(10));
3202 assert_eq!(time.now(), std::time::Duration::from_secs(10));
3203 }
3204
3205 #[test]
3206 fn cancellation_test_completes_normally() {
3207 let test = CancellationTest::new(EchoHandler);
3208 let result = test.complete_normally();
3209
3210 assert!(result.completed);
3211 assert!(!result.cancelled_at_checkpoint);
3212 assert!(result.response.is_some());
3213 assert_eq!(result.response.as_ref().unwrap().status().as_u16(), 200);
3214 }
3215
3216 #[test]
3217 fn cancellation_test_respects_cancellation() {
3218 struct CheckpointHandler;
3220
3221 impl Handler for CheckpointHandler {
3222 fn call<'a>(
3223 &'a self,
3224 ctx: &'a RequestContext,
3225 _req: &'a mut Request,
3226 ) -> BoxFuture<'a, Response> {
3227 Box::pin(async move {
3228 if ctx.checkpoint().is_err() {
3230 return Response::with_status(StatusCode::CLIENT_CLOSED_REQUEST);
3231 }
3232 Response::ok().body(ResponseBody::Bytes(b"OK".to_vec()))
3233 })
3234 }
3235 }
3236
3237 let test = CancellationTest::new(CheckpointHandler);
3238 let result = test.test_respects_cancellation();
3239
3240 assert!(result.completed);
3241 assert!(result.cancelled_at_checkpoint);
3242 assert!(result.response.is_some());
3243 assert_eq!(result.response.as_ref().unwrap().status().as_u16(), 499);
3245 }
3246
3247 #[test]
3248 fn cancellation_test_result_helpers() {
3249 let graceful = CancellationTestResult {
3250 completed: false,
3251 cancelled_at_checkpoint: true,
3252 response: None,
3253 cancellation_point: None,
3254 };
3255 assert!(graceful.gracefully_cancelled());
3256 assert!(!graceful.completed_despite_cancel());
3257
3258 let completed = CancellationTestResult {
3259 completed: true,
3260 cancelled_at_checkpoint: false,
3261 response: Some(Response::ok()),
3262 cancellation_point: None,
3263 };
3264 assert!(!completed.gracefully_cancelled());
3265 assert!(completed.completed_despite_cancel());
3266 }
3267
3268 #[test]
3273 fn test_logger_captures_all_levels() {
3274 let logger = TestLogger::new();
3275
3276 logger.log_message(LogLevel::Debug, "debug message", 1);
3277 logger.log_message(LogLevel::Info, "info message", 1);
3278 logger.log_message(LogLevel::Warn, "warn message", 1);
3279 logger.log_message(LogLevel::Error, "error message", 1);
3280
3281 let logs = logger.logs();
3282 assert_eq!(logs.len(), 4);
3283
3284 assert_eq!(logs[0].level, LogLevel::Debug);
3285 assert_eq!(logs[1].level, LogLevel::Info);
3286 assert_eq!(logs[2].level, LogLevel::Warn);
3287 assert_eq!(logs[3].level, LogLevel::Error);
3288 }
3289
3290 #[test]
3291 fn test_logger_logs_at_level_filters_correctly() {
3292 let logger = TestLogger::new();
3293
3294 logger.log_message(LogLevel::Debug, "debug", 1);
3295 logger.log_message(LogLevel::Info, "info 1", 1);
3296 logger.log_message(LogLevel::Info, "info 2", 2);
3297 logger.log_message(LogLevel::Warn, "warn", 1);
3298 logger.log_message(LogLevel::Error, "error", 1);
3299
3300 let info_logs = logger.logs_at_level(LogLevel::Info);
3301 assert_eq!(info_logs.len(), 2);
3302 assert!(info_logs[0].contains("info 1"));
3303 assert!(info_logs[1].contains("info 2"));
3304
3305 let error_logs = logger.logs_at_level(LogLevel::Error);
3306 assert_eq!(error_logs.len(), 1);
3307 assert!(error_logs[0].contains("error"));
3308
3309 let trace_logs = logger.logs_at_level(LogLevel::Trace);
3310 assert_eq!(trace_logs.len(), 0);
3311 }
3312
3313 #[test]
3314 fn test_logger_contains_message_search() {
3315 let logger = TestLogger::new();
3316
3317 logger.log_message(LogLevel::Info, "User alice logged in", 100);
3318 logger.log_message(LogLevel::Info, "Request processed for /api/users", 101);
3319 logger.log_message(LogLevel::Warn, "Rate limit approaching for alice", 102);
3320
3321 assert!(logger.contains_message("alice"));
3322 assert!(logger.contains_message("/api/users"));
3323 assert!(logger.contains_message("Rate limit"));
3324 assert!(!logger.contains_message("bob"));
3325 assert!(!logger.contains_message("nonexistent"));
3326 }
3327
3328 #[test]
3329 fn test_logger_contains_multiple_messages() {
3330 let logger = TestLogger::new();
3331
3332 logger.log_message(LogLevel::Info, "step 1 complete", 1);
3333 logger.log_message(LogLevel::Info, "step 2 complete", 2);
3334 logger.log_message(LogLevel::Info, "step 3 complete", 3);
3335
3336 assert!(logger.contains_message("step 1"));
3338 assert!(logger.contains_message("step 2"));
3339 assert!(logger.contains_message("step 3"));
3340 assert!(logger.contains_message("complete"));
3341 assert!(!logger.contains_message("step 4"));
3343 }
3344
3345 #[test]
3346 fn test_log_capture_captures_logs_in_closure() {
3347 let capture = TestLogger::capture(|logger| {
3348 logger.log_message(LogLevel::Info, "inside capture", 1);
3349 logger.log_message(LogLevel::Warn, "warning inside", 2);
3350 42
3351 });
3352
3353 assert!(capture.passed());
3354 assert!(!capture.failed());
3355 assert_eq!(capture.result, Some(42));
3356 assert_eq!(capture.logs.len(), 2);
3357 assert!(capture.contains_message("inside capture"));
3358 assert!(capture.contains_message("warning inside"));
3359 }
3360
3361 #[test]
3362 fn test_log_capture_count_by_level() {
3363 let capture = TestLogger::capture(|logger| {
3364 logger.log_message(LogLevel::Info, "info 1", 1);
3365 logger.log_message(LogLevel::Info, "info 2", 2);
3366 logger.log_message(LogLevel::Info, "info 3", 3);
3367 logger.log_message(LogLevel::Error, "error 1", 4);
3368 });
3369
3370 assert_eq!(capture.count_by_level(LogLevel::Info), 3);
3371 assert_eq!(capture.count_by_level(LogLevel::Error), 1);
3372 assert_eq!(capture.count_by_level(LogLevel::Warn), 0);
3373 }
3374
3375 #[test]
3376 fn test_log_capture_phased_all_phases() {
3377 let capture = TestLogger::capture_phased(
3378 |logger| {
3379 logger.log_message(LogLevel::Info, "setup phase", 1);
3380 },
3381 |logger| {
3382 logger.log_message(LogLevel::Info, "execute phase", 2);
3383 "result"
3384 },
3385 |logger| {
3386 logger.log_message(LogLevel::Info, "teardown phase", 3);
3387 },
3388 );
3389
3390 assert!(capture.passed());
3391 assert_eq!(capture.result, Some("result"));
3392 assert_eq!(capture.logs.len(), 3);
3393 assert!(capture.contains_message("setup phase"));
3394 assert!(capture.contains_message("execute phase"));
3395 assert!(capture.contains_message("teardown phase"));
3396 }
3397
3398 #[test]
3399 fn test_log_capture_timings_recorded() {
3400 let capture = TestLogger::capture(|_logger| {
3401 let mut sum = 0;
3403 for i in 0..1000 {
3404 sum += i;
3405 }
3406 sum
3407 });
3408
3409 assert!(capture.passed());
3410 let timings = &capture.timings;
3412 assert!(timings.total() >= std::time::Duration::ZERO);
3413 }
3414
3415 #[test]
3416 fn test_log_capture_failure_context() {
3417 let capture = TestLogger::capture(|logger| {
3418 logger.log_message(LogLevel::Info, "step 1", 1);
3419 logger.log_message(LogLevel::Info, "step 2", 2);
3420 logger.log_message(LogLevel::Error, "something went wrong", 3);
3421 logger.log_message(LogLevel::Info, "step 3", 4);
3422 });
3423
3424 let context = capture.failure_context(3);
3425 assert!(context.contains("something went wrong") || context.contains("step 3"));
3427 }
3428
3429 #[test]
3430 fn test_captured_log_format() {
3431 let log = CapturedLog::new(LogLevel::Warn, "test warning message", 12345);
3432
3433 let formatted = log.format();
3434 assert!(formatted.contains("[W]"));
3436 assert!(formatted.contains("test warning message"));
3437 assert!(formatted.contains("12345"));
3438 }
3439
3440 #[test]
3441 fn test_captured_log_contains() {
3442 let log = CapturedLog::new(LogLevel::Info, "user login successful for alice", 1);
3443
3444 assert!(log.contains("login"));
3445 assert!(log.contains("alice"));
3446 assert!(log.contains("successful"));
3447 assert!(!log.contains("bob"));
3448 assert!(!log.contains("failed"));
3449 }
3450
3451 #[test]
3452 fn test_captured_log_fields() {
3453 let log = CapturedLog::new(LogLevel::Error, "database connection failed", 999);
3454
3455 assert_eq!(log.level, LogLevel::Error);
3456 assert_eq!(log.message, "database connection failed");
3457 assert_eq!(log.request_id, 999);
3458 }
3459
3460 #[test]
3461 fn test_multiple_loggers_isolated() {
3462 let logger1 = TestLogger::new();
3463 let logger2 = TestLogger::new();
3464
3465 logger1.log_message(LogLevel::Info, "from logger 1", 1);
3466 logger2.log_message(LogLevel::Info, "from logger 2", 2);
3467
3468 assert_eq!(logger1.logs().len(), 1);
3469 assert_eq!(logger2.logs().len(), 1);
3470 assert!(logger1.contains_message("logger 1"));
3471 assert!(!logger1.contains_message("logger 2"));
3472 assert!(logger2.contains_message("logger 2"));
3473 assert!(!logger2.contains_message("logger 1"));
3474 }
3475
3476 #[test]
3477 fn test_logger_log_entry_integration() {
3478 let logger = TestLogger::new();
3479
3480 let entry = LogEntry {
3481 level: LogLevel::Warn,
3482 message: "warning from entry".to_string(),
3483 request_id: 42,
3484 region_id: "region-1".to_string(),
3485 task_id: "task-1".to_string(),
3486 target: None,
3487 fields: Vec::new(),
3488 timestamp_ns: 0,
3489 };
3490
3491 logger.log_entry(&entry);
3492
3493 assert_eq!(logger.logs().len(), 1);
3494 let captured = &logger.logs()[0];
3495 assert_eq!(captured.level, LogLevel::Warn);
3496 assert!(captured.contains("warning from entry"));
3497 assert_eq!(captured.request_id, 42);
3498 }
3499
3500 #[test]
3501 fn test_log_capture_unwrap_on_success() {
3502 let capture = TestLogger::capture(|_| 123);
3503 let value = capture.unwrap();
3504 assert_eq!(value, 123);
3505 }
3506
3507 #[test]
3508 fn test_log_capture_unwrap_or_on_success() {
3509 let capture = TestLogger::capture(|_| 456);
3510 let value = capture.unwrap_or(0);
3511 assert_eq!(value, 456);
3512 }
3513}
3514
3515use std::io::{Read as _, Write as _};
3520use std::net::{Shutdown, SocketAddr, TcpListener as StdTcpListener, TcpStream as StdTcpStream};
3521use std::sync::atomic::AtomicBool;
3522use std::thread;
3523use std::time::Duration;
3524
3525fn graceful_close(stream: &StdTcpStream) {
3532 let _ = stream.shutdown(Shutdown::Write);
3533}
3534
3535#[derive(Debug, Clone)]
3540pub struct RecordedRequest {
3541 pub method: String,
3543 pub path: String,
3545 pub query: Option<String>,
3547 pub headers: Vec<(String, String)>,
3549 pub body: Vec<u8>,
3551 pub timestamp: std::time::Instant,
3553}
3554
3555impl RecordedRequest {
3556 #[must_use]
3562 pub fn body_text(&self) -> &str {
3563 std::str::from_utf8(&self.body).expect("body is not valid UTF-8")
3564 }
3565
3566 #[must_use]
3568 pub fn header(&self, name: &str) -> Option<&str> {
3569 let name_lower = name.to_ascii_lowercase();
3570 self.headers
3571 .iter()
3572 .find(|(n, _)| n.to_ascii_lowercase() == name_lower)
3573 .map(|(_, v)| v.as_str())
3574 }
3575
3576 #[must_use]
3578 pub fn url(&self) -> String {
3579 match &self.query {
3580 Some(q) => format!("{}?{}", self.path, q),
3581 None => self.path.clone(),
3582 }
3583 }
3584}
3585
3586#[derive(Debug, Clone)]
3588pub struct MockResponse {
3589 pub status: u16,
3591 pub headers: Vec<(String, String)>,
3593 pub body: Vec<u8>,
3595 pub delay: Option<Duration>,
3597}
3598
3599impl Default for MockResponse {
3600 fn default() -> Self {
3601 Self {
3602 status: 200,
3603 headers: vec![("content-type".to_string(), "text/plain".to_string())],
3604 body: b"OK".to_vec(),
3605 delay: None,
3606 }
3607 }
3608}
3609
3610impl MockResponse {
3611 #[must_use]
3613 pub fn ok() -> Self {
3614 Self::default()
3615 }
3616
3617 #[must_use]
3619 pub fn with_status(status: u16) -> Self {
3620 Self {
3621 status,
3622 ..Default::default()
3623 }
3624 }
3625
3626 #[must_use]
3628 pub fn status(mut self, status: u16) -> Self {
3629 self.status = status;
3630 self
3631 }
3632
3633 #[must_use]
3635 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
3636 self.headers.push((name.into(), value.into()));
3637 self
3638 }
3639
3640 #[must_use]
3642 pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
3643 self.body = body.into();
3644 self
3645 }
3646
3647 #[must_use]
3649 pub fn body_str(self, body: &str) -> Self {
3650 self.body(body.as_bytes().to_vec())
3651 }
3652
3653 #[must_use]
3655 pub fn json<T: serde::Serialize>(mut self, value: &T) -> Self {
3656 self.body = serde_json::to_vec(value).expect("JSON serialization failed");
3657 self.headers
3658 .push(("content-type".to_string(), "application/json".to_string()));
3659 self
3660 }
3661
3662 #[must_use]
3664 pub fn delay(mut self, duration: Duration) -> Self {
3665 self.delay = Some(duration);
3666 self
3667 }
3668
3669 fn to_http_response(&self) -> Vec<u8> {
3671 let status_text = match self.status {
3672 200 => "OK",
3673 201 => "Created",
3674 204 => "No Content",
3675 400 => "Bad Request",
3676 401 => "Unauthorized",
3677 403 => "Forbidden",
3678 404 => "Not Found",
3679 500 => "Internal Server Error",
3680 502 => "Bad Gateway",
3681 503 => "Service Unavailable",
3682 504 => "Gateway Timeout",
3683 _ => "Unknown",
3684 };
3685
3686 let mut response = format!("HTTP/1.1 {} {}\r\n", self.status, status_text);
3687
3688 response.push_str(&format!("content-length: {}\r\n", self.body.len()));
3690
3691 for (name, value) in &self.headers {
3693 response.push_str(&format!("{}: {}\r\n", name, value));
3694 }
3695
3696 response.push_str("\r\n");
3697
3698 let mut bytes = response.into_bytes();
3699 bytes.extend_from_slice(&self.body);
3700 bytes
3701 }
3702}
3703
3704pub struct MockServer {
3738 addr: SocketAddr,
3739 requests: Arc<Mutex<Vec<RecordedRequest>>>,
3740 responses: Arc<Mutex<HashMap<String, MockResponse>>>,
3741 default_response: Arc<Mutex<MockResponse>>,
3742 shutdown: Arc<AtomicBool>,
3743 handle: Option<thread::JoinHandle<()>>,
3744}
3745
3746impl MockServer {
3747 #[must_use]
3758 pub fn start() -> Self {
3759 Self::start_with_options(MockServerOptions::default())
3760 }
3761
3762 #[must_use]
3764 pub fn start_with_options(options: MockServerOptions) -> Self {
3765 let listener =
3767 StdTcpListener::bind("127.0.0.1:0").expect("Failed to bind mock server to port");
3768 let addr = listener.local_addr().expect("Failed to get local address");
3769
3770 listener
3772 .set_nonblocking(true)
3773 .expect("Failed to set non-blocking");
3774
3775 let requests = Arc::new(Mutex::new(Vec::new()));
3776 let responses = Arc::new(Mutex::new(HashMap::new()));
3777 let default_response = Arc::new(Mutex::new(options.default_response));
3778 let shutdown = Arc::new(AtomicBool::new(false));
3779
3780 let requests_clone = Arc::clone(&requests);
3781 let responses_clone = Arc::clone(&responses);
3782 let default_response_clone = Arc::clone(&default_response);
3783 let shutdown_clone = Arc::clone(&shutdown);
3784 let read_timeout = options.read_timeout;
3785
3786 let handle = thread::spawn(move || {
3787 Self::server_loop(
3788 listener,
3789 requests_clone,
3790 responses_clone,
3791 default_response_clone,
3792 shutdown_clone,
3793 read_timeout,
3794 );
3795 });
3796
3797 Self {
3798 addr,
3799 requests,
3800 responses,
3801 default_response,
3802 shutdown,
3803 handle: Some(handle),
3804 }
3805 }
3806
3807 fn server_loop(
3809 listener: StdTcpListener,
3810 requests: Arc<Mutex<Vec<RecordedRequest>>>,
3811 responses: Arc<Mutex<HashMap<String, MockResponse>>>,
3812 default_response: Arc<Mutex<MockResponse>>,
3813 shutdown: Arc<AtomicBool>,
3814 read_timeout: Duration,
3815 ) {
3816 loop {
3817 if shutdown.load(std::sync::atomic::Ordering::Acquire) {
3818 break;
3819 }
3820
3821 match listener.accept() {
3822 Ok((stream, _peer)) => {
3823 let _ = stream.set_nonblocking(false);
3828
3829 let requests = Arc::clone(&requests);
3831 let responses = Arc::clone(&responses);
3832 let default_response = Arc::clone(&default_response);
3833
3834 Self::handle_connection(
3836 stream,
3837 requests,
3838 responses,
3839 default_response,
3840 read_timeout,
3841 );
3842 }
3843 Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
3844 thread::sleep(Duration::from_millis(10));
3846 }
3847 Err(e) => {
3848 eprintln!("MockServer accept error: {}", e);
3849 break;
3850 }
3851 }
3852 }
3853 }
3854
3855 fn handle_connection(
3857 mut stream: StdTcpStream,
3858 requests: Arc<Mutex<Vec<RecordedRequest>>>,
3859 responses: Arc<Mutex<HashMap<String, MockResponse>>>,
3860 default_response: Arc<Mutex<MockResponse>>,
3861 read_timeout: Duration,
3862 ) {
3863 let _ = stream.set_read_timeout(Some(read_timeout));
3865
3866 let mut buffer = vec![0u8; 8192];
3868 let Ok(bytes_read) = stream.read(&mut buffer) else {
3869 return;
3870 };
3871
3872 if bytes_read == 0 {
3873 return;
3874 }
3875
3876 buffer.truncate(bytes_read);
3877
3878 let Some(recorded) = Self::parse_request(&buffer) else {
3880 return;
3881 };
3882
3883 {
3885 let mut reqs = requests.lock();
3886 reqs.push(recorded.clone());
3887 }
3888
3889 let response = {
3891 let resps = responses.lock();
3892 match resps.get(&recorded.path) {
3893 Some(r) => r.clone(),
3894 None => {
3895 let mut matched = None;
3897 for (pattern, resp) in resps.iter() {
3898 if pattern.ends_with('*') {
3899 let prefix = &pattern[..pattern.len() - 1];
3900 if recorded.path.starts_with(prefix) {
3901 matched = Some(resp.clone());
3902 break;
3903 }
3904 }
3905 }
3906 matched.unwrap_or_else(|| default_response.lock().clone())
3907 }
3908 }
3909 };
3910
3911 if let Some(delay) = response.delay {
3913 thread::sleep(delay);
3914 }
3915
3916 let response_bytes = response.to_http_response();
3918 let _ = stream.write_all(&response_bytes);
3919 let _ = stream.flush();
3920 graceful_close(&stream);
3921 }
3922
3923 fn parse_request(data: &[u8]) -> Option<RecordedRequest> {
3925 let text = std::str::from_utf8(data).ok()?;
3926 let mut lines = text.lines();
3927
3928 let request_line = lines.next()?;
3930 let parts: Vec<&str> = request_line.split_whitespace().collect();
3931 if parts.len() < 2 {
3932 return None;
3933 }
3934
3935 let method = parts[0].to_string();
3936 let full_path = parts[1];
3937
3938 let (path, query) = if let Some(idx) = full_path.find('?') {
3940 (
3941 full_path[..idx].to_string(),
3942 Some(full_path[idx + 1..].to_string()),
3943 )
3944 } else {
3945 (full_path.to_string(), None)
3946 };
3947
3948 let mut headers = Vec::new();
3950 let mut content_length = 0usize;
3951 for line in lines.by_ref() {
3952 if line.is_empty() {
3953 break;
3954 }
3955 if let Some((name, value)) = line.split_once(':') {
3956 let name = name.trim().to_string();
3957 let value = value.trim().to_string();
3958 if name.eq_ignore_ascii_case("content-length") {
3959 content_length = value.parse().unwrap_or(0);
3960 }
3961 headers.push((name, value));
3962 }
3963 }
3964
3965 let body = if content_length > 0 {
3967 if let Some(body_start) = text.find("\r\n\r\n") {
3969 let body_start = body_start + 4;
3970 if body_start < data.len() {
3971 data[body_start..].to_vec()
3972 } else {
3973 Vec::new()
3974 }
3975 } else if let Some(body_start) = text.find("\n\n") {
3976 let body_start = body_start + 2;
3977 if body_start < data.len() {
3978 data[body_start..].to_vec()
3979 } else {
3980 Vec::new()
3981 }
3982 } else {
3983 Vec::new()
3984 }
3985 } else {
3986 Vec::new()
3987 };
3988
3989 Some(RecordedRequest {
3990 method,
3991 path,
3992 query,
3993 headers,
3994 body,
3995 timestamp: std::time::Instant::now(),
3996 })
3997 }
3998
3999 #[must_use]
4001 pub fn addr(&self) -> SocketAddr {
4002 self.addr
4003 }
4004
4005 #[must_use]
4007 pub fn url(&self) -> String {
4008 format!("http://{}", self.addr)
4009 }
4010
4011 #[must_use]
4013 pub fn url_for(&self, path: &str) -> String {
4014 let path = if path.starts_with('/') {
4015 path
4016 } else {
4017 &format!("/{}", path)
4018 };
4019 format!("http://{}{}", self.addr, path)
4020 }
4021
4022 pub fn mock_response(&self, path: impl Into<String>, response: MockResponse) {
4033 let mut responses = self.responses.lock();
4034 responses.insert(path.into(), response);
4035 }
4036
4037 pub fn set_default_response(&self, response: MockResponse) {
4039 let mut default = self.default_response.lock();
4040 *default = response;
4041 }
4042
4043 #[must_use]
4045 pub fn requests(&self) -> Vec<RecordedRequest> {
4046 let requests = self.requests.lock();
4047 requests.clone()
4048 }
4049
4050 #[must_use]
4052 pub fn request_count(&self) -> usize {
4053 let requests = self.requests.lock();
4054 requests.len()
4055 }
4056
4057 #[must_use]
4059 pub fn requests_for(&self, path: &str) -> Vec<RecordedRequest> {
4060 let requests = self.requests.lock();
4061 requests
4062 .iter()
4063 .filter(|r| r.path == path)
4064 .cloned()
4065 .collect()
4066 }
4067
4068 #[must_use]
4070 pub fn last_request(&self) -> Option<RecordedRequest> {
4071 let requests = self.requests.lock();
4072 requests.last().cloned()
4073 }
4074
4075 pub fn clear_requests(&self) {
4077 let mut requests = self.requests.lock();
4078 requests.clear();
4079 }
4080
4081 pub fn clear_responses(&self) {
4083 let mut responses = self.responses.lock();
4084 responses.clear();
4085 }
4086
4087 pub fn reset(&self) {
4089 self.clear_requests();
4090 self.clear_responses();
4091 }
4092
4093 pub fn wait_for_requests(&self, count: usize, timeout: Duration) -> bool {
4098 let start = std::time::Instant::now();
4099 loop {
4100 if self.request_count() >= count {
4101 return true;
4102 }
4103 if start.elapsed() >= timeout {
4104 return false;
4105 }
4106 thread::sleep(Duration::from_millis(10));
4107 }
4108 }
4109
4110 pub fn assert_received(&self, path: &str) {
4116 let requests = self.requests_for(path);
4117 assert!(
4118 !requests.is_empty(),
4119 "Expected request to path '{}', but none was received. Received paths: {:?}",
4120 path,
4121 self.requests().iter().map(|r| &r.path).collect::<Vec<_>>()
4122 );
4123 }
4124
4125 pub fn assert_not_received(&self, path: &str) {
4131 let requests = self.requests_for(path);
4132 assert!(
4133 requests.is_empty(),
4134 "Expected no request to path '{}', but {} were received",
4135 path,
4136 requests.len()
4137 );
4138 }
4139
4140 pub fn assert_request_count(&self, expected: usize) {
4146 let actual = self.request_count();
4147 assert_eq!(
4148 actual, expected,
4149 "Expected {} requests, but received {}",
4150 expected, actual
4151 );
4152 }
4153}
4154
4155impl Drop for MockServer {
4156 fn drop(&mut self) {
4157 self.shutdown
4159 .store(true, std::sync::atomic::Ordering::Release);
4160
4161 if let Some(handle) = self.handle.take() {
4163 let _ = handle.join();
4164 }
4165 }
4166}
4167
4168#[derive(Debug, Clone)]
4170pub struct MockServerOptions {
4171 pub default_response: MockResponse,
4173 pub read_timeout: Duration,
4175}
4176
4177impl Default for MockServerOptions {
4178 fn default() -> Self {
4179 Self {
4180 default_response: MockResponse::with_status(404).body_str("Not Found"),
4181 read_timeout: Duration::from_secs(5),
4182 }
4183 }
4184}
4185
4186impl MockServerOptions {
4187 #[must_use]
4189 pub fn new() -> Self {
4190 Self::default()
4191 }
4192
4193 #[must_use]
4195 pub fn default_response(mut self, response: MockResponse) -> Self {
4196 self.default_response = response;
4197 self
4198 }
4199
4200 #[must_use]
4202 pub fn read_timeout(mut self, timeout: Duration) -> Self {
4203 self.read_timeout = timeout;
4204 self
4205 }
4206}
4207
4208#[derive(Debug, Clone)]
4217pub struct TestServerLogEntry {
4218 pub method: String,
4220 pub path: String,
4222 pub status: u16,
4224 pub duration: Duration,
4226 pub timestamp: std::time::Instant,
4228}
4229
4230#[derive(Debug, Clone)]
4232pub struct TestServerConfig {
4233 pub read_timeout: Duration,
4235 pub log_requests: bool,
4237}
4238
4239impl Default for TestServerConfig {
4240 fn default() -> Self {
4241 Self {
4242 read_timeout: Duration::from_secs(5),
4243 log_requests: true,
4244 }
4245 }
4246}
4247
4248impl TestServerConfig {
4249 #[must_use]
4251 pub fn new() -> Self {
4252 Self::default()
4253 }
4254
4255 #[must_use]
4257 pub fn read_timeout(mut self, timeout: Duration) -> Self {
4258 self.read_timeout = timeout;
4259 self
4260 }
4261
4262 #[must_use]
4264 pub fn log_requests(mut self, log: bool) -> Self {
4265 self.log_requests = log;
4266 self
4267 }
4268}
4269
4270pub struct TestServer {
4330 addr: SocketAddr,
4331 shutdown: Arc<AtomicBool>,
4332 handle: Option<thread::JoinHandle<()>>,
4333 log_entries: Arc<Mutex<Vec<TestServerLogEntry>>>,
4334 shutdown_controller: crate::shutdown::ShutdownController,
4335}
4336
4337impl TestServer {
4338 #[must_use]
4348 pub fn start(app: crate::app::App) -> Self {
4349 Self::start_with_config(app, TestServerConfig::default())
4350 }
4351
4352 #[must_use]
4354 pub fn start_with_config(app: crate::app::App, config: TestServerConfig) -> Self {
4355 let listener =
4356 StdTcpListener::bind("127.0.0.1:0").expect("Failed to bind test server to port");
4357 let addr = listener.local_addr().expect("Failed to get local address");
4358
4359 listener
4360 .set_nonblocking(true)
4361 .expect("Failed to set non-blocking");
4362
4363 let app = Arc::new(app);
4364 let shutdown = Arc::new(AtomicBool::new(false));
4365 let log_entries = Arc::new(Mutex::new(Vec::new()));
4366 let shutdown_controller = crate::shutdown::ShutdownController::new();
4367
4368 let shutdown_clone = Arc::clone(&shutdown);
4369 let log_entries_clone = Arc::clone(&log_entries);
4370 let app_clone = Arc::clone(&app);
4371 let controller_clone = shutdown_controller.clone();
4372
4373 let handle = thread::spawn(move || {
4374 Self::server_loop(
4375 listener,
4376 app_clone,
4377 shutdown_clone,
4378 log_entries_clone,
4379 config,
4380 controller_clone,
4381 );
4382 });
4383
4384 Self {
4385 addr,
4386 shutdown,
4387 handle: Some(handle),
4388 log_entries,
4389 shutdown_controller,
4390 }
4391 }
4392
4393 fn server_loop(
4395 listener: StdTcpListener,
4396 app: Arc<crate::app::App>,
4397 shutdown: Arc<AtomicBool>,
4398 log_entries: Arc<Mutex<Vec<TestServerLogEntry>>>,
4399 config: TestServerConfig,
4400 controller: crate::shutdown::ShutdownController,
4401 ) {
4402 let request_counter = std::sync::atomic::AtomicU64::new(1);
4403
4404 loop {
4405 if shutdown.load(std::sync::atomic::Ordering::Acquire) {
4406 while let Some(hook) = controller.pop_hook() {
4408 hook.run();
4409 }
4410 break;
4411 }
4412
4413 match listener.accept() {
4414 Ok((stream, _peer)) => {
4415 let _guard = controller.track_request();
4417
4418 let _ = stream.set_nonblocking(false);
4424
4425 if controller.is_shutting_down() {
4427 Self::send_503(stream);
4428 continue;
4429 }
4430
4431 Self::handle_connection(stream, &app, &log_entries, &config, &request_counter);
4432 }
4433 Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
4434 thread::sleep(Duration::from_millis(5));
4435 }
4436 Err(_) => {
4437 break;
4438 }
4439 }
4440 }
4441 }
4442
4443 fn handle_connection(
4445 mut stream: StdTcpStream,
4446 app: &Arc<crate::app::App>,
4447 log_entries: &Arc<Mutex<Vec<TestServerLogEntry>>>,
4448 config: &TestServerConfig,
4449 request_counter: &std::sync::atomic::AtomicU64,
4450 ) {
4451 let _ = stream.set_read_timeout(Some(config.read_timeout));
4452
4453 let mut buffer = vec![0u8; 65536];
4455 let bytes_read = match stream.read(&mut buffer) {
4456 Ok(n) if n > 0 => n,
4457 _ => return,
4458 };
4459 buffer.truncate(bytes_read);
4460
4461 let Some(parsed) = Self::parse_raw_request(&buffer) else {
4463 let bad_request = b"HTTP/1.1 400 Bad Request\r\ncontent-length: 11\r\n\r\nBad Request";
4465 let _ = stream.write_all(bad_request);
4466 let _ = stream.flush();
4467 graceful_close(&stream);
4468 return;
4469 };
4470
4471 let start_time = std::time::Instant::now();
4472
4473 let method = match parsed.method.to_uppercase().as_str() {
4475 "GET" => Method::Get,
4476 "POST" => Method::Post,
4477 "PUT" => Method::Put,
4478 "DELETE" => Method::Delete,
4479 "PATCH" => Method::Patch,
4480 "HEAD" => Method::Head,
4481 "OPTIONS" => Method::Options,
4482 _ => Method::Get,
4483 };
4484
4485 let mut request = Request::new(method, &parsed.path);
4486
4487 if let Some(ref query) = parsed.query {
4489 request.set_query(Some(query.clone()));
4490 }
4491
4492 for (name, value) in &parsed.headers {
4494 request
4495 .headers_mut()
4496 .insert(name.clone(), value.as_bytes().to_vec());
4497 }
4498
4499 if !parsed.body.is_empty() {
4501 request.set_body(Body::Bytes(parsed.body.clone()));
4502 }
4503
4504 let cx = Cx::for_testing();
4506 let request_id = request_counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
4507 let dependency_overrides = Handler::dependency_overrides(app.as_ref())
4508 .unwrap_or_else(|| Arc::new(crate::dependency::DependencyOverrides::new()));
4509 let ctx = RequestContext::with_overrides(cx, request_id, dependency_overrides);
4510
4511 let response = futures_executor::block_on(app.handle(&ctx, &mut request));
4513
4514 let duration = start_time.elapsed();
4515 let status_code = response.status().as_u16();
4516
4517 if config.log_requests {
4519 let entry = TestServerLogEntry {
4520 method: parsed.method.clone(),
4521 path: parsed.path.clone(),
4522 status: status_code,
4523 duration,
4524 timestamp: start_time,
4525 };
4526 log_entries.lock().push(entry);
4527 }
4528
4529 let response_bytes = Self::serialize_response(response);
4531 let _ = stream.write_all(&response_bytes);
4532 let _ = stream.flush();
4533 graceful_close(&stream);
4534 }
4535
4536 fn parse_raw_request(data: &[u8]) -> Option<ParsedRequest> {
4538 let text = std::str::from_utf8(data).ok()?;
4539 let mut lines = text.lines();
4540
4541 let request_line = lines.next()?;
4543 let parts: Vec<&str> = request_line.split_whitespace().collect();
4544 if parts.len() < 2 {
4545 return None;
4546 }
4547
4548 let method = parts[0].to_string();
4549 let full_path = parts[1];
4550
4551 let (path, query) = if let Some(idx) = full_path.find('?') {
4553 (
4554 full_path[..idx].to_string(),
4555 Some(full_path[idx + 1..].to_string()),
4556 )
4557 } else {
4558 (full_path.to_string(), None)
4559 };
4560
4561 let mut headers = Vec::new();
4563 let mut content_length = 0usize;
4564 for line in lines.by_ref() {
4565 if line.is_empty() {
4566 break;
4567 }
4568 if let Some((name, value)) = line.split_once(':') {
4569 let name = name.trim().to_string();
4570 let value = value.trim().to_string();
4571 if name.eq_ignore_ascii_case("content-length") {
4572 content_length = value.parse().unwrap_or(0);
4573 }
4574 headers.push((name, value));
4575 }
4576 }
4577
4578 let body = if content_length > 0 {
4580 if let Some(body_start) = text.find("\r\n\r\n") {
4581 let body_start = body_start + 4;
4582 if body_start < data.len() {
4583 data[body_start..].to_vec()
4584 } else {
4585 Vec::new()
4586 }
4587 } else if let Some(body_start) = text.find("\n\n") {
4588 let body_start = body_start + 2;
4589 if body_start < data.len() {
4590 data[body_start..].to_vec()
4591 } else {
4592 Vec::new()
4593 }
4594 } else {
4595 Vec::new()
4596 }
4597 } else {
4598 Vec::new()
4599 };
4600
4601 Some(ParsedRequest {
4602 method,
4603 path,
4604 query,
4605 headers,
4606 body,
4607 })
4608 }
4609
4610 fn serialize_response(response: Response) -> Vec<u8> {
4612 let (status, headers, body) = response.into_parts();
4613
4614 let body_bytes = match body {
4615 ResponseBody::Empty => Vec::new(),
4616 ResponseBody::Bytes(b) => b,
4617 ResponseBody::Stream(_) => {
4618 Vec::new()
4621 }
4622 };
4623
4624 let mut buf = Vec::with_capacity(512 + body_bytes.len());
4625
4626 buf.extend_from_slice(b"HTTP/1.1 ");
4628 buf.extend_from_slice(status.as_u16().to_string().as_bytes());
4629 buf.extend_from_slice(b" ");
4630 buf.extend_from_slice(status.canonical_reason().as_bytes());
4631 buf.extend_from_slice(b"\r\n");
4632
4633 for (name, value) in &headers {
4635 if name.eq_ignore_ascii_case("content-length")
4636 || name.eq_ignore_ascii_case("transfer-encoding")
4637 {
4638 continue;
4639 }
4640 buf.extend_from_slice(name.as_bytes());
4641 buf.extend_from_slice(b": ");
4642 buf.extend_from_slice(value);
4643 buf.extend_from_slice(b"\r\n");
4644 }
4645
4646 buf.extend_from_slice(b"content-length: ");
4648 buf.extend_from_slice(body_bytes.len().to_string().as_bytes());
4649 buf.extend_from_slice(b"\r\n");
4650
4651 buf.extend_from_slice(b"\r\n");
4653
4654 buf.extend_from_slice(&body_bytes);
4656
4657 buf
4658 }
4659
4660 #[must_use]
4662 pub fn addr(&self) -> SocketAddr {
4663 self.addr
4664 }
4665
4666 #[must_use]
4668 pub fn port(&self) -> u16 {
4669 self.addr.port()
4670 }
4671
4672 #[must_use]
4674 pub fn url(&self) -> String {
4675 format!("http://{}", self.addr)
4676 }
4677
4678 #[must_use]
4680 pub fn url_for(&self, path: &str) -> String {
4681 let path = if path.starts_with('/') {
4682 path.to_string()
4683 } else {
4684 format!("/{path}")
4685 };
4686 format!("http://{}{}", self.addr, path)
4687 }
4688
4689 #[must_use]
4691 pub fn log_entries(&self) -> Vec<TestServerLogEntry> {
4692 self.log_entries.lock().clone()
4693 }
4694
4695 #[must_use]
4697 pub fn request_count(&self) -> usize {
4698 self.log_entries.lock().len()
4699 }
4700
4701 pub fn clear_logs(&self) {
4703 self.log_entries.lock().clear();
4704 }
4705
4706 fn send_503(mut stream: StdTcpStream) {
4708 let response =
4709 b"HTTP/1.1 503 Service Unavailable\r\ncontent-length: 19\r\n\r\nService Unavailable";
4710 let _ = stream.write_all(response);
4711 let _ = stream.flush();
4712 graceful_close(&stream);
4713 }
4714
4715 #[must_use]
4722 pub fn shutdown_controller(&self) -> &crate::shutdown::ShutdownController {
4723 &self.shutdown_controller
4724 }
4725
4726 #[must_use]
4728 pub fn in_flight_count(&self) -> usize {
4729 self.shutdown_controller.in_flight_count()
4730 }
4731
4732 pub fn shutdown(&self) {
4738 self.shutdown_controller.shutdown();
4739 self.shutdown
4740 .store(true, std::sync::atomic::Ordering::Release);
4741 }
4742
4743 #[must_use]
4745 pub fn is_shutdown(&self) -> bool {
4746 self.shutdown.load(std::sync::atomic::Ordering::Acquire)
4747 }
4748}
4749
4750impl Drop for TestServer {
4751 fn drop(&mut self) {
4752 self.shutdown
4753 .store(true, std::sync::atomic::Ordering::Release);
4754 if let Some(handle) = self.handle.take() {
4755 let _ = handle.join();
4756 }
4757 }
4758}
4759
4760struct ParsedRequest {
4762 method: String,
4763 path: String,
4764 query: Option<String>,
4765 headers: Vec<(String, String)>,
4766 body: Vec<u8>,
4767}
4768
4769#[derive(Debug, Clone)]
4775pub enum E2EStepResult {
4776 Passed,
4778 Failed(String),
4780 Skipped,
4782}
4783
4784impl E2EStepResult {
4785 #[must_use]
4787 pub fn is_passed(&self) -> bool {
4788 matches!(self, Self::Passed)
4789 }
4790
4791 #[must_use]
4793 pub fn is_failed(&self) -> bool {
4794 matches!(self, Self::Failed(_))
4795 }
4796}
4797
4798#[derive(Debug, Clone)]
4800pub struct E2ECapture {
4801 pub method: String,
4803 pub path: String,
4805 pub request_headers: Vec<(String, String)>,
4807 pub request_body: Option<String>,
4809 pub response_status: u16,
4811 pub response_headers: Vec<(String, String)>,
4813 pub response_body: String,
4815}
4816
4817#[derive(Debug, Clone)]
4819pub struct E2EStep {
4820 pub name: String,
4822 pub started_at: std::time::Instant,
4824 pub duration: std::time::Duration,
4826 pub result: E2EStepResult,
4828 pub capture: Option<E2ECapture>,
4830}
4831
4832impl E2EStep {
4833 fn new(name: impl Into<String>) -> Self {
4835 Self {
4836 name: name.into(),
4837 started_at: std::time::Instant::now(),
4838 duration: std::time::Duration::ZERO,
4839 result: E2EStepResult::Skipped,
4840 capture: None,
4841 }
4842 }
4843
4844 fn complete(&mut self, result: E2EStepResult) {
4846 self.duration = self.started_at.elapsed();
4847 self.result = result;
4848 }
4849}
4850
4851pub struct E2EScenario<H> {
4882 name: String,
4884 description: Option<String>,
4886 client: TestClient<H>,
4888 steps: Vec<E2EStep>,
4890 stop_on_failure: bool,
4892 has_failure: bool,
4894 log_buffer: Vec<String>,
4896}
4897
4898impl<H: Handler + 'static> E2EScenario<H> {
4899 pub fn new(name: impl Into<String>, client: TestClient<H>) -> Self {
4901 let name = name.into();
4902 Self {
4903 name,
4904 description: None,
4905 client,
4906 steps: Vec::new(),
4907 stop_on_failure: true,
4908 has_failure: false,
4909 log_buffer: Vec::new(),
4910 }
4911 }
4912
4913 #[must_use]
4915 pub fn description(mut self, desc: impl Into<String>) -> Self {
4916 self.description = Some(desc.into());
4917 self
4918 }
4919
4920 #[must_use]
4922 pub fn stop_on_failure(mut self, stop: bool) -> Self {
4923 self.stop_on_failure = stop;
4924 self
4925 }
4926
4927 pub fn client(&self) -> &TestClient<H> {
4929 &self.client
4930 }
4931
4932 pub fn client_mut(&mut self) -> &mut TestClient<H> {
4934 &mut self.client
4935 }
4936
4937 pub fn log(&mut self, message: impl Into<String>) {
4939 let msg = message.into();
4940 self.log_buffer.push(format!(
4941 "[{:?}] {}",
4942 std::time::Instant::now().elapsed(),
4943 msg
4944 ));
4945 }
4946
4947 pub fn step<F>(&mut self, name: impl Into<String>, f: F)
4952 where
4953 F: FnOnce(&TestClient<H>) + std::panic::UnwindSafe,
4954 {
4955 let name = name.into();
4956 let mut step = E2EStep::new(&name);
4957
4958 if self.has_failure && self.stop_on_failure {
4960 step.complete(E2EStepResult::Skipped);
4961 self.log_buffer.push(format!("[SKIP] {}", name));
4962 self.steps.push(step);
4963 return;
4964 }
4965
4966 self.log_buffer.push(format!("[START] {}", name));
4967
4968 let client_ref = std::panic::AssertUnwindSafe(&self.client);
4970
4971 let result = std::panic::catch_unwind(|| {
4973 f(&client_ref);
4974 });
4975
4976 match result {
4977 Ok(()) => {
4978 step.complete(E2EStepResult::Passed);
4979 self.log_buffer
4980 .push(format!("[PASS] {} ({:?})", name, step.duration));
4981 }
4982 Err(panic_info) => {
4983 let error_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
4984 (*s).to_string()
4985 } else if let Some(s) = panic_info.downcast_ref::<String>() {
4986 s.clone()
4987 } else {
4988 "Unknown panic".to_string()
4989 };
4990
4991 step.complete(E2EStepResult::Failed(error_msg.clone()));
4992 self.has_failure = true;
4993 self.log_buffer
4994 .push(format!("[FAIL] {} - {}", name, error_msg));
4995 }
4996 }
4997
4998 self.steps.push(step);
4999 }
5000
5001 pub fn try_step<F, E>(&mut self, name: impl Into<String>, f: F) -> Result<(), E>
5003 where
5004 F: FnOnce(&TestClient<H>) -> Result<(), E>,
5005 E: std::fmt::Display,
5006 {
5007 let name = name.into();
5008 let mut step = E2EStep::new(&name);
5009
5010 if self.has_failure && self.stop_on_failure {
5011 step.complete(E2EStepResult::Skipped);
5012 self.steps.push(step);
5013 return Ok(());
5014 }
5015
5016 self.log_buffer.push(format!("[START] {}", name));
5017
5018 match f(&self.client) {
5019 Ok(()) => {
5020 step.complete(E2EStepResult::Passed);
5021 self.log_buffer
5022 .push(format!("[PASS] {} ({:?})", name, step.duration));
5023 self.steps.push(step);
5024 Ok(())
5025 }
5026 Err(e) => {
5027 let error_msg = e.to_string();
5028 step.complete(E2EStepResult::Failed(error_msg.clone()));
5029 self.has_failure = true;
5030 self.log_buffer
5031 .push(format!("[FAIL] {} - {}", name, error_msg));
5032 self.steps.push(step);
5033 Err(e)
5034 }
5035 }
5036 }
5037
5038 #[must_use]
5040 pub fn passed(&self) -> bool {
5041 !self.has_failure
5042 }
5043
5044 #[must_use]
5046 pub fn steps(&self) -> &[E2EStep] {
5047 &self.steps
5048 }
5049
5050 #[must_use]
5052 pub fn logs(&self) -> &[String] {
5053 &self.log_buffer
5054 }
5055
5056 #[must_use]
5058 pub fn report(&self) -> E2EReport {
5059 let passed = self.steps.iter().filter(|s| s.result.is_passed()).count();
5060 let failed = self.steps.iter().filter(|s| s.result.is_failed()).count();
5061 let skipped = self
5062 .steps
5063 .iter()
5064 .filter(|s| matches!(s.result, E2EStepResult::Skipped))
5065 .count();
5066 let total_duration: std::time::Duration = self.steps.iter().map(|s| s.duration).sum();
5067
5068 E2EReport {
5069 scenario_name: self.name.clone(),
5070 description: self.description.clone(),
5071 passed,
5072 failed,
5073 skipped,
5074 total_duration,
5075 steps: self.steps.clone(),
5076 logs: self.log_buffer.clone(),
5077 }
5078 }
5079
5080 pub fn assert_passed(&self) {
5084 if !self.passed() {
5085 let report = self.report();
5086 panic!(
5087 "E2E Scenario '{}' failed!\n\n{}",
5088 self.name,
5089 report.to_text()
5090 );
5091 }
5092 }
5093}
5094
5095#[derive(Debug, Clone)]
5097pub struct E2EReport {
5098 pub scenario_name: String,
5100 pub description: Option<String>,
5102 pub passed: usize,
5104 pub failed: usize,
5106 pub skipped: usize,
5108 pub total_duration: std::time::Duration,
5110 pub steps: Vec<E2EStep>,
5112 pub logs: Vec<String>,
5114}
5115
5116impl E2EReport {
5117 #[must_use]
5119 pub fn to_text(&self) -> String {
5120 let mut output = String::new();
5121
5122 output.push_str(&format!("E2E Test Report: {}\n", self.scenario_name));
5124 output.push_str(&"=".repeat(60));
5125 output.push('\n');
5126
5127 if let Some(desc) = &self.description {
5128 output.push_str(&format!("Description: {}\n", desc));
5129 }
5130
5131 output.push_str(&format!(
5133 "\nSummary: {} passed, {} failed, {} skipped\n",
5134 self.passed, self.failed, self.skipped
5135 ));
5136 output.push_str(&format!("Total Duration: {:?}\n", self.total_duration));
5137 output.push_str(&"-".repeat(60));
5138 output.push('\n');
5139
5140 output.push_str("\nSteps:\n");
5142 for (i, step) in self.steps.iter().enumerate() {
5143 let status = match &step.result {
5144 E2EStepResult::Passed => "[PASS]",
5145 E2EStepResult::Failed(_) => "[FAIL]",
5146 E2EStepResult::Skipped => "[SKIP]",
5147 };
5148 output.push_str(&format!(
5149 " {}. {} {} ({:?})\n",
5150 i + 1,
5151 status,
5152 step.name,
5153 step.duration
5154 ));
5155 if let E2EStepResult::Failed(msg) = &step.result {
5156 output.push_str(&format!(" Error: {}\n", msg));
5157 }
5158 }
5159
5160 if !self.logs.is_empty() {
5162 output.push_str(&"-".repeat(60));
5163 output.push_str("\n\nLogs:\n");
5164 for log in &self.logs {
5165 output.push_str(&format!(" {}\n", log));
5166 }
5167 }
5168
5169 output
5170 }
5171
5172 #[must_use]
5174 pub fn to_json(&self) -> String {
5175 let steps_json: Vec<String> = self
5176 .steps
5177 .iter()
5178 .map(|step| {
5179 let status = match &step.result {
5180 E2EStepResult::Passed => "passed",
5181 E2EStepResult::Failed(_) => "failed",
5182 E2EStepResult::Skipped => "skipped",
5183 };
5184 let error = match &step.result {
5185 E2EStepResult::Failed(msg) => format!(r#", "error": "{}""#, escape_json(msg)),
5186 _ => String::new(),
5187 };
5188 format!(
5189 r#" {{ "name": "{}", "status": "{}", "duration_ms": {}{} }}"#,
5190 escape_json(&step.name),
5191 status,
5192 step.duration.as_millis(),
5193 error
5194 )
5195 })
5196 .collect();
5197
5198 format!(
5199 r#"{{
5200 "scenario": "{}",
5201 "description": {},
5202 "summary": {{
5203 "passed": {},
5204 "failed": {},
5205 "skipped": {},
5206 "total_duration_ms": {}
5207 }},
5208 "steps": [
5209{}
5210 ]
5211}}"#,
5212 escape_json(&self.scenario_name),
5213 self.description
5214 .as_ref()
5215 .map_or("null".to_string(), |d| format!(r#""{}""#, escape_json(d))),
5216 self.passed,
5217 self.failed,
5218 self.skipped,
5219 self.total_duration.as_millis(),
5220 steps_json.join(",\n")
5221 )
5222 }
5223
5224 #[must_use]
5226 pub fn to_html(&self) -> String {
5227 let status_class = if self.failed > 0 { "failed" } else { "passed" };
5228
5229 use std::fmt::Write;
5230 let steps_html =
5231 self.steps
5232 .iter()
5233 .enumerate()
5234 .fold(String::new(), |mut output, (i, step)| {
5235 let (status, class) = match &step.result {
5236 E2EStepResult::Passed => ("✓", "pass"),
5237 E2EStepResult::Failed(_) => ("✗", "fail"),
5238 E2EStepResult::Skipped => ("○", "skip"),
5239 };
5240 let error_html = match &step.result {
5241 E2EStepResult::Failed(msg) => {
5242 format!(r#"<div class="error">{}</div>"#, escape_html(msg))
5243 }
5244 _ => String::new(),
5245 };
5246 let _ = write!(
5247 output,
5248 r#" <tr class="{}">
5249 <td>{}</td>
5250 <td><span class="status">{}</span></td>
5251 <td>{}</td>
5252 <td>{:?}</td>
5253 </tr>
5254 {}"#,
5255 class,
5256 i + 1,
5257 status,
5258 escape_html(&step.name),
5259 step.duration,
5260 error_html
5261 );
5262 output
5263 });
5264
5265 format!(
5266 r#"<!DOCTYPE html>
5267<html>
5268<head>
5269 <title>E2E Report: {}</title>
5270 <style>
5271 body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 2rem; }}
5272 h1 {{ color: #333; }}
5273 .summary {{ padding: 1rem; border-radius: 8px; margin: 1rem 0; }}
5274 .summary.passed {{ background: #d4edda; }}
5275 .summary.failed {{ background: #f8d7da; }}
5276 table {{ width: 100%; border-collapse: collapse; margin-top: 1rem; }}
5277 th, td {{ padding: 0.75rem; text-align: left; border-bottom: 1px solid #dee2e6; }}
5278 th {{ background: #f8f9fa; }}
5279 .pass {{ color: #28a745; }}
5280 .fail {{ color: #dc3545; }}
5281 .skip {{ color: #6c757d; }}
5282 .status {{ font-size: 1.2rem; }}
5283 .error {{ color: #dc3545; font-size: 0.9rem; padding: 0.5rem; background: #fff; margin-top: 0.25rem; }}
5284 </style>
5285</head>
5286<body>
5287 <h1>E2E Report: {}</h1>
5288 {}
5289 <div class="summary {}">
5290 <strong>Summary:</strong> {} passed, {} failed, {} skipped<br>
5291 <strong>Duration:</strong> {:?}
5292 </div>
5293 <table>
5294 <thead>
5295 <tr><th>#</th><th>Status</th><th>Step</th><th>Duration</th></tr>
5296 </thead>
5297 <tbody>
5298{}
5299 </tbody>
5300 </table>
5301</body>
5302</html>"#,
5303 escape_html(&self.scenario_name),
5304 escape_html(&self.scenario_name),
5305 self.description
5306 .as_ref()
5307 .map_or(String::new(), |d| format!("<p>{}</p>", escape_html(d))),
5308 status_class,
5309 self.passed,
5310 self.failed,
5311 self.skipped,
5312 self.total_duration,
5313 steps_html
5314 )
5315 }
5316}
5317
5318fn escape_json(s: &str) -> String {
5320 s.replace('\\', "\\\\")
5321 .replace('"', "\\\"")
5322 .replace('\n', "\\n")
5323 .replace('\r', "\\r")
5324 .replace('\t', "\\t")
5325}
5326
5327fn escape_html(s: &str) -> String {
5329 s.replace('&', "&")
5330 .replace('<', "<")
5331 .replace('>', ">")
5332 .replace('"', """)
5333}
5334
5335#[macro_export]
5368macro_rules! e2e_test {
5369 (
5370 name: $name:expr,
5371 $(description: $desc:expr,)?
5372 client: $client:expr,
5373 $(step $step_name:literal => |$client_param:ident| $step_body:block),+ $(,)?
5374 ) => {{
5375 let client = $client;
5376 let mut scenario = $crate::testing::E2EScenario::new($name, client);
5377 $(
5378 scenario = scenario.description($desc);
5379 )?
5380 $(
5381 scenario.step($step_name, |$client_param| $step_body);
5382 )+
5383 scenario.assert_passed();
5384 scenario.report()
5385 }};
5386}
5387
5388pub use e2e_test;
5389
5390use crate::logging::{LogEntry, LogLevel};
5395
5396#[derive(Debug, Clone)]
5398pub struct CapturedLog {
5399 pub level: LogLevel,
5401 pub message: String,
5403 pub request_id: u64,
5405 pub captured_at: std::time::Instant,
5407 pub fields: Vec<(String, String)>,
5409 pub target: Option<String>,
5411}
5412
5413impl CapturedLog {
5414 pub fn from_entry(entry: &LogEntry) -> Self {
5416 Self {
5417 level: entry.level,
5418 message: entry.message.clone(),
5419 request_id: entry.request_id,
5420 captured_at: std::time::Instant::now(),
5421 fields: entry.fields.clone(),
5422 target: entry.target.clone(),
5423 }
5424 }
5425
5426 pub fn new(level: LogLevel, message: impl Into<String>, request_id: u64) -> Self {
5428 Self {
5429 level,
5430 message: message.into(),
5431 request_id,
5432 captured_at: std::time::Instant::now(),
5433 fields: Vec::new(),
5434 target: None,
5435 }
5436 }
5437
5438 #[must_use]
5440 pub fn contains(&self, text: &str) -> bool {
5441 self.message.contains(text)
5442 }
5443
5444 #[must_use]
5446 pub fn format(&self) -> String {
5447 let mut output = format!(
5448 "[{}] req={} {}",
5449 self.level.as_char(),
5450 self.request_id,
5451 self.message
5452 );
5453 if !self.fields.is_empty() {
5454 output.push_str(" {");
5455 for (i, (k, v)) in self.fields.iter().enumerate() {
5456 if i > 0 {
5457 output.push_str(", ");
5458 }
5459 output.push_str(&format!("{k}={v}"));
5460 }
5461 output.push('}');
5462 }
5463 output
5464 }
5465}
5466
5467#[derive(Debug, Clone)]
5492pub struct TestLogger {
5493 logs: Arc<Mutex<Vec<CapturedLog>>>,
5495 timings: Arc<Mutex<TestTimings>>,
5497 echo_logs: bool,
5499}
5500
5501#[derive(Debug, Clone, Default)]
5503pub struct TestTimings {
5504 pub setup: Option<std::time::Duration>,
5506 pub execute: Option<std::time::Duration>,
5508 pub teardown: Option<std::time::Duration>,
5510 phase_start: Option<std::time::Instant>,
5512}
5513
5514impl TestTimings {
5515 pub fn start_phase(&mut self) {
5517 self.phase_start = Some(std::time::Instant::now());
5518 }
5519
5520 pub fn end_setup(&mut self) {
5522 if let Some(start) = self.phase_start.take() {
5523 self.setup = Some(start.elapsed());
5524 }
5525 }
5526
5527 pub fn end_execute(&mut self) {
5529 if let Some(start) = self.phase_start.take() {
5530 self.execute = Some(start.elapsed());
5531 }
5532 }
5533
5534 pub fn end_teardown(&mut self) {
5536 if let Some(start) = self.phase_start.take() {
5537 self.teardown = Some(start.elapsed());
5538 }
5539 }
5540
5541 #[must_use]
5543 pub fn total(&self) -> std::time::Duration {
5544 self.setup.unwrap_or_default()
5545 + self.execute.unwrap_or_default()
5546 + self.teardown.unwrap_or_default()
5547 }
5548
5549 #[must_use]
5551 pub fn format(&self) -> String {
5552 format!(
5553 "Timings: setup={:?}, execute={:?}, teardown={:?}, total={:?}",
5554 self.setup.unwrap_or_default(),
5555 self.execute.unwrap_or_default(),
5556 self.teardown.unwrap_or_default(),
5557 self.total()
5558 )
5559 }
5560}
5561
5562impl TestLogger {
5563 pub fn new() -> Self {
5565 Self {
5566 logs: Arc::new(Mutex::new(Vec::new())),
5567 timings: Arc::new(Mutex::new(TestTimings::default())),
5568 echo_logs: std::env::var("FASTAPI_TEST_ECHO_LOGS").is_ok(),
5569 }
5570 }
5571
5572 pub fn with_echo() -> Self {
5574 let mut logger = Self::new();
5575 logger.echo_logs = true;
5576 logger
5577 }
5578
5579 pub fn log(&self, entry: CapturedLog) {
5581 if self.echo_logs {
5582 eprintln!("[LOG] {}", entry.format());
5583 }
5584 self.logs.lock().push(entry);
5585 }
5586
5587 pub fn log_entry(&self, entry: &LogEntry) {
5589 self.log(CapturedLog::from_entry(entry));
5590 }
5591
5592 pub fn log_message(&self, level: LogLevel, message: impl Into<String>, request_id: u64) {
5594 self.log(CapturedLog::new(level, message, request_id));
5595 }
5596
5597 #[must_use]
5599 pub fn logs(&self) -> Vec<CapturedLog> {
5600 self.logs.lock().clone()
5601 }
5602
5603 #[must_use]
5605 pub fn count(&self) -> usize {
5606 self.logs.lock().len()
5607 }
5608
5609 pub fn clear(&self) {
5611 self.logs.lock().clear();
5612 }
5613
5614 #[must_use]
5616 pub fn contains_message(&self, text: &str) -> bool {
5617 self.logs.lock().iter().any(|log| log.contains(text))
5618 }
5619
5620 #[must_use]
5622 pub fn count_by_level(&self, level: LogLevel) -> usize {
5623 self.logs
5624 .lock()
5625 .iter()
5626 .filter(|log| log.level == level)
5627 .count()
5628 }
5629
5630 #[must_use]
5632 pub fn logs_at_level(&self, level: LogLevel) -> Vec<CapturedLog> {
5633 self.logs
5634 .lock()
5635 .iter()
5636 .filter(|log| log.level == level)
5637 .cloned()
5638 .collect()
5639 }
5640
5641 #[must_use]
5643 pub fn failure_context(&self, n: usize) -> String {
5644 let logs = self.logs.lock();
5645 let start = logs.len().saturating_sub(n);
5646 let recent: Vec<_> = logs[start..].iter().map(CapturedLog::format).collect();
5647
5648 if recent.is_empty() {
5649 "No logs captured".to_string()
5650 } else {
5651 format!(
5652 "Last {} log(s) before failure:\n {}",
5653 recent.len(),
5654 recent.join("\n ")
5655 )
5656 }
5657 }
5658
5659 #[must_use]
5661 pub fn timings(&self) -> TestTimings {
5662 self.timings.lock().clone()
5663 }
5664
5665 pub fn start_phase(&self) {
5667 self.timings.lock().start_phase();
5668 }
5669
5670 pub fn end_setup(&self) {
5672 self.timings.lock().end_setup();
5673 }
5674
5675 pub fn end_execute(&self) {
5677 self.timings.lock().end_execute();
5678 }
5679
5680 pub fn end_teardown(&self) {
5682 self.timings.lock().end_teardown();
5683 }
5684
5685 pub fn capture<F, T>(f: F) -> LogCapture<T>
5689 where
5690 F: FnOnce(&TestLogger) -> T,
5691 {
5692 let logger = TestLogger::new();
5693
5694 logger.start_phase();
5695 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5696 logger.end_setup();
5697 logger.start_phase();
5698 let result = f(&logger);
5699 logger.end_execute();
5700 result
5701 }));
5702
5703 let (ok_result, panic_info) = match result {
5704 Ok(v) => (Some(v), None),
5705 Err(p) => {
5706 let msg = if let Some(s) = p.downcast_ref::<&str>() {
5707 (*s).to_string()
5708 } else if let Some(s) = p.downcast_ref::<String>() {
5709 s.clone()
5710 } else {
5711 "Unknown panic".to_string()
5712 };
5713 (None, Some(msg))
5714 }
5715 };
5716
5717 LogCapture {
5718 logs: logger.logs(),
5719 timings: logger.timings(),
5720 result: ok_result,
5721 panic_info,
5722 }
5723 }
5724
5725 pub fn capture_phased<S, E, D, T>(setup: S, execute: E, teardown: D) -> LogCapture<T>
5727 where
5728 S: FnOnce(&TestLogger),
5729 E: FnOnce(&TestLogger) -> T,
5730 D: FnOnce(&TestLogger),
5731 {
5732 let logger = TestLogger::new();
5733
5734 logger.start_phase();
5736 let setup_panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5737 setup(&logger);
5738 }));
5739 logger.end_setup();
5740
5741 if setup_panic.is_err() {
5742 return LogCapture {
5743 logs: logger.logs(),
5744 timings: logger.timings(),
5745 result: None,
5746 panic_info: Some("Setup phase panicked".to_string()),
5747 };
5748 }
5749
5750 logger.start_phase();
5752 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| execute(&logger)));
5753 logger.end_execute();
5754
5755 logger.start_phase();
5757 let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
5758 teardown(&logger);
5759 }));
5760 logger.end_teardown();
5761
5762 let (ok_result, panic_info) = match result {
5763 Ok(v) => (Some(v), None),
5764 Err(p) => {
5765 let msg = if let Some(s) = p.downcast_ref::<&str>() {
5766 (*s).to_string()
5767 } else if let Some(s) = p.downcast_ref::<String>() {
5768 s.clone()
5769 } else {
5770 "Unknown panic".to_string()
5771 };
5772 (None, Some(msg))
5773 }
5774 };
5775
5776 LogCapture {
5777 logs: logger.logs(),
5778 timings: logger.timings(),
5779 result: ok_result,
5780 panic_info,
5781 }
5782 }
5783}
5784
5785impl Default for TestLogger {
5786 fn default() -> Self {
5787 Self::new()
5788 }
5789}
5790
5791#[derive(Debug)]
5793pub struct LogCapture<T> {
5794 pub logs: Vec<CapturedLog>,
5796 pub timings: TestTimings,
5798 pub result: Option<T>,
5800 pub panic_info: Option<String>,
5802}
5803
5804impl<T> LogCapture<T> {
5805 #[must_use]
5807 pub fn passed(&self) -> bool {
5808 self.result.is_some()
5809 }
5810
5811 #[must_use]
5813 pub fn failed(&self) -> bool {
5814 self.panic_info.is_some()
5815 }
5816
5817 #[must_use]
5819 pub fn contains_message(&self, text: &str) -> bool {
5820 self.logs.iter().any(|log| log.contains(text))
5821 }
5822
5823 #[must_use]
5825 pub fn count_by_level(&self, level: LogLevel) -> usize {
5826 self.logs.iter().filter(|log| log.level == level).count()
5827 }
5828
5829 #[must_use]
5831 pub fn failure_context(&self, n: usize) -> String {
5832 let start = self.logs.len().saturating_sub(n);
5833 let recent: Vec<_> = self.logs[start..].iter().map(CapturedLog::format).collect();
5834
5835 let mut output = String::new();
5836
5837 if let Some(ref panic) = self.panic_info {
5838 output.push_str(&format!("Test failed: {}\n\n", panic));
5839 }
5840
5841 output.push_str(&self.timings.format());
5842 output.push_str("\n\n");
5843
5844 if recent.is_empty() {
5845 output.push_str("No logs captured");
5846 } else {
5847 output.push_str(&format!(
5848 "Last {} log(s) before failure:\n {}",
5849 recent.len(),
5850 recent.join("\n ")
5851 ));
5852 }
5853
5854 output
5855 }
5856
5857 pub fn unwrap(self) -> T {
5859 match self.result {
5860 Some(v) => v,
5861 None => panic!(
5862 "Test failed with log context:\n{}",
5863 self.failure_context(10)
5864 ),
5865 }
5866 }
5867
5868 pub fn unwrap_or(self, default: T) -> T {
5870 self.result.unwrap_or(default)
5871 }
5872}
5873
5874#[macro_export]
5879macro_rules! assert_with_logs {
5880 ($logger:expr, $cond:expr) => {
5881 if !$cond {
5882 panic!(
5883 "Assertion failed: {}\n\n{}",
5884 stringify!($cond),
5885 $logger.failure_context(10)
5886 );
5887 }
5888 };
5889 ($logger:expr, $cond:expr, $($arg:tt)+) => {
5890 if !$cond {
5891 panic!(
5892 "Assertion failed: {}\n\n{}",
5893 format!($($arg)+),
5894 $logger.failure_context(10)
5895 );
5896 }
5897 };
5898}
5899
5900#[macro_export]
5902macro_rules! assert_eq_with_logs {
5903 ($logger:expr, $left:expr, $right:expr) => {
5904 if $left != $right {
5905 panic!(
5906 "Assertion failed: {} == {}\n left: {:?}\n right: {:?}\n\n{}",
5907 stringify!($left),
5908 stringify!($right),
5909 $left,
5910 $right,
5911 $logger.failure_context(10)
5912 );
5913 }
5914 };
5915 ($logger:expr, $left:expr, $right:expr, $($arg:tt)+) => {
5916 if $left != $right {
5917 panic!(
5918 "Assertion failed: {}\n left: {:?}\n right: {:?}\n\n{}",
5919 format!($($arg)+),
5920 $left,
5921 $right,
5922 $logger.failure_context(10)
5923 );
5924 }
5925 };
5926}
5927
5928pub use assert_eq_with_logs;
5929pub use assert_with_logs;
5930
5931#[derive(Debug)]
5933pub struct ResponseDiff {
5934 pub expected_status: u16,
5936 pub actual_status: u16,
5938 pub expected_body: Option<String>,
5940 pub actual_body: String,
5942 pub header_diffs: Vec<(String, Option<String>, Option<String>)>,
5944}
5945
5946impl ResponseDiff {
5947 pub fn new(expected_status: u16, actual: &TestResponse) -> Self {
5949 Self {
5950 expected_status,
5951 actual_status: actual.status().as_u16(),
5952 expected_body: None,
5953 actual_body: actual.text().to_string(),
5954 header_diffs: Vec::new(),
5955 }
5956 }
5957
5958 #[must_use]
5960 pub fn expected_body(mut self, body: impl Into<String>) -> Self {
5961 self.expected_body = Some(body.into());
5962 self
5963 }
5964
5965 #[must_use]
5967 pub fn expected_header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
5968 self.header_diffs
5969 .push((name.into(), Some(value.into()), None));
5970 self
5971 }
5972
5973 #[must_use]
5975 pub fn is_match(&self) -> bool {
5976 if self.expected_status != self.actual_status {
5977 return false;
5978 }
5979 if let Some(ref expected) = self.expected_body {
5980 if !self.actual_body.contains(expected) {
5981 return false;
5982 }
5983 }
5984 true
5985 }
5986
5987 #[must_use]
5989 pub fn format(&self) -> String {
5990 let mut output = String::new();
5991
5992 if self.expected_status != self.actual_status {
5993 output.push_str(&format!(
5994 "Status mismatch:\n expected: {}\n actual: {}\n",
5995 self.expected_status, self.actual_status
5996 ));
5997 }
5998
5999 if let Some(ref expected) = self.expected_body {
6000 if !self.actual_body.contains(expected) {
6001 output.push_str(&format!(
6002 "Body mismatch:\n expected to contain: {:?}\n actual: {:?}\n",
6003 expected, self.actual_body
6004 ));
6005 }
6006 }
6007
6008 for (name, expected, actual) in &self.header_diffs {
6009 output.push_str(&format!(
6010 "Header '{}' mismatch:\n expected: {:?}\n actual: {:?}\n",
6011 name, expected, actual
6012 ));
6013 }
6014
6015 if output.is_empty() {
6016 "No differences".to_string()
6017 } else {
6018 output
6019 }
6020 }
6021}
6022
6023#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
6047pub struct ResponseSnapshot {
6048 pub status: u16,
6050 pub headers: Vec<(String, String)>,
6052 pub body: String,
6054 #[serde(skip_serializing_if = "Option::is_none")]
6056 pub body_json: Option<serde_json::Value>,
6057}
6058
6059impl ResponseSnapshot {
6060 pub fn from_test_response(resp: &TestResponse) -> Self {
6065 let body = resp.text().to_string();
6066 let body_json = serde_json::from_str::<serde_json::Value>(&body).ok();
6067
6068 let mut headers: Vec<(String, String)> = resp
6069 .headers()
6070 .iter()
6071 .filter_map(|(name, value)| {
6072 std::str::from_utf8(value)
6073 .ok()
6074 .map(|v| (name.to_lowercase(), v.to_string()))
6075 })
6076 .collect();
6077 headers.sort();
6078
6079 Self {
6080 status: resp.status().as_u16(),
6081 headers,
6082 body,
6083 body_json,
6084 }
6085 }
6086
6087 pub fn from_test_response_with_headers(resp: &TestResponse, header_names: &[&str]) -> Self {
6089 let mut snapshot = Self::from_test_response(resp);
6090 let names: Vec<String> = header_names.iter().map(|n| n.to_lowercase()).collect();
6091 snapshot.headers.retain(|(name, _)| names.contains(name));
6092 snapshot
6093 }
6094
6095 #[must_use]
6102 pub fn mask_fields(mut self, paths: &[&str], placeholder: &str) -> Self {
6103 if let Some(ref mut json) = self.body_json {
6104 for path in paths {
6105 mask_json_path(json, path, placeholder);
6106 }
6107 self.body = serde_json::to_string_pretty(json).unwrap_or(self.body);
6108 }
6109 self
6110 }
6111
6112 pub fn save(&self, path: impl AsRef<std::path::Path>) -> std::io::Result<()> {
6114 let path = path.as_ref();
6115 if let Some(parent) = path.parent() {
6116 std::fs::create_dir_all(parent)?;
6117 }
6118 let json = serde_json::to_string_pretty(self).map_err(std::io::Error::other)?;
6119 std::fs::write(path, json)
6120 }
6121
6122 pub fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
6124 let data = std::fs::read_to_string(path)?;
6125 serde_json::from_str(&data)
6126 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
6127 }
6128
6129 #[must_use]
6131 pub fn diff(&self, other: &Self) -> String {
6132 let mut output = String::new();
6133
6134 if self.status != other.status {
6135 output.push_str(&format!("Status: {} vs {}\n", self.status, other.status));
6136 }
6137
6138 for (name, value) in &self.headers {
6140 match other.headers.iter().find(|(n, _)| n == name) {
6141 Some((_, other_value)) if value != other_value => {
6142 output.push_str(&format!(
6143 "Header '{}': {:?} vs {:?}\n",
6144 name, value, other_value
6145 ));
6146 }
6147 None => {
6148 output.push_str(&format!("Header '{}': present vs missing\n", name));
6149 }
6150 _ => {}
6151 }
6152 }
6153 for (name, _) in &other.headers {
6154 if !self.headers.iter().any(|(n, _)| n == name) {
6155 output.push_str(&format!("Header '{}': missing vs present\n", name));
6156 }
6157 }
6158
6159 if self.body != other.body {
6161 output.push_str(&format!(
6162 "Body:\n expected: {:?}\n actual: {:?}\n",
6163 other.body, self.body
6164 ));
6165 }
6166
6167 if output.is_empty() {
6168 "No differences".to_string()
6169 } else {
6170 output
6171 }
6172 }
6173
6174 pub fn matches_ignoring_headers(&self, other: &Self, ignore: &[&str]) -> bool {
6176 if self.status != other.status {
6177 return false;
6178 }
6179
6180 let ignore_lower: Vec<String> = ignore.iter().map(|s| s.to_lowercase()).collect();
6181
6182 let self_headers: Vec<_> = self
6183 .headers
6184 .iter()
6185 .filter(|(n, _)| !ignore_lower.contains(n))
6186 .collect();
6187 let other_headers: Vec<_> = other
6188 .headers
6189 .iter()
6190 .filter(|(n, _)| !ignore_lower.contains(n))
6191 .collect();
6192
6193 if self_headers != other_headers {
6194 return false;
6195 }
6196
6197 match (&self.body_json, &other.body_json) {
6199 (Some(a), Some(b)) => a == b,
6200 _ => self.body == other.body,
6201 }
6202 }
6203}
6204
6205fn mask_json_path(value: &mut serde_json::Value, path: &str, placeholder: &str) {
6207 let parts: Vec<&str> = path.splitn(2, '.').collect();
6208 match parts.as_slice() {
6209 [key] => {
6210 if let Some(obj) = value.as_object_mut() {
6211 if obj.contains_key(*key) {
6212 obj.insert(
6213 key.to_string(),
6214 serde_json::Value::String(placeholder.to_string()),
6215 );
6216 }
6217 }
6218 if let Some(arr) = value.as_array_mut() {
6219 if let Ok(idx) = key.parse::<usize>() {
6220 if idx < arr.len() {
6221 arr[idx] = serde_json::Value::String(placeholder.to_string());
6222 }
6223 }
6224 }
6225 }
6226 [key, rest] => {
6227 if let Some(obj) = value.as_object_mut() {
6228 if let Some(child) = obj.get_mut(*key) {
6229 mask_json_path(child, rest, placeholder);
6230 }
6231 }
6232 if let Some(arr) = value.as_array_mut() {
6233 if let Ok(idx) = key.parse::<usize>() {
6234 if let Some(child) = arr.get_mut(idx) {
6235 mask_json_path(child, rest, placeholder);
6236 }
6237 }
6238 }
6239 }
6240 _ => {}
6241 }
6242}
6243
6244#[macro_export]
6259macro_rules! assert_response_snapshot {
6260 ($response:expr, $path:expr) => {{
6261 let snapshot = $crate::ResponseSnapshot::from_test_response(&$response);
6262 let path = std::path::Path::new($path);
6263
6264 if std::env::var("SNAPSHOT_UPDATE").is_ok() || !path.exists() {
6265 snapshot.save(path).expect("failed to save snapshot");
6266 } else {
6267 let expected =
6268 $crate::ResponseSnapshot::load(path).expect("failed to load snapshot");
6269 assert!(
6270 snapshot == expected,
6271 "Snapshot mismatch for {}:\n{}",
6272 $path,
6273 snapshot.diff(&expected)
6274 );
6275 }
6276 }};
6277 ($response:expr, $path:expr, mask: [$($field:expr),* $(,)?]) => {{
6278 let snapshot = $crate::ResponseSnapshot::from_test_response(&$response)
6279 .mask_fields(&[$($field),*], "<MASKED>");
6280 let path = std::path::Path::new($path);
6281
6282 if std::env::var("SNAPSHOT_UPDATE").is_ok() || !path.exists() {
6283 snapshot.save(path).expect("failed to save snapshot");
6284 } else {
6285 let expected =
6286 $crate::ResponseSnapshot::load(path).expect("failed to load snapshot");
6287 assert!(
6288 snapshot == expected,
6289 "Snapshot mismatch for {}:\n{}",
6290 $path,
6291 snapshot.diff(&expected)
6292 );
6293 }
6294 }};
6295}
6296
6297#[cfg(test)]
6298mod snapshot_tests {
6299 use super::*;
6300
6301 fn mock_test_response(status: u16, body: &str, headers: &[(&str, &str)]) -> TestResponse {
6302 let mut resp =
6303 crate::response::Response::with_status(crate::response::StatusCode::from_u16(status));
6304 for (name, value) in headers {
6305 resp = resp.header(*name, value.as_bytes().to_vec());
6306 }
6307 resp = resp.body(crate::response::ResponseBody::Bytes(
6308 body.as_bytes().to_vec(),
6309 ));
6310 TestResponse::new(resp, 0)
6311 }
6312
6313 #[test]
6314 fn snapshot_from_test_response() {
6315 let resp = mock_test_response(
6316 200,
6317 r#"{"id":1,"name":"Alice"}"#,
6318 &[("content-type", "application/json")],
6319 );
6320 let snap = ResponseSnapshot::from_test_response(&resp);
6321
6322 assert_eq!(snap.status, 200);
6323 assert!(snap.body_json.is_some());
6324 assert_eq!(snap.body_json.as_ref().unwrap()["name"], "Alice");
6325 }
6326
6327 #[test]
6328 fn snapshot_equality() {
6329 let resp = mock_test_response(200, "hello", &[]);
6330 let snap1 = ResponseSnapshot::from_test_response(&resp);
6331 let snap2 = ResponseSnapshot::from_test_response(&resp);
6332 assert_eq!(snap1, snap2);
6333 }
6334
6335 #[test]
6336 fn snapshot_diff_status() {
6337 let s1 = ResponseSnapshot {
6338 status: 200,
6339 headers: vec![],
6340 body: "ok".to_string(),
6341 body_json: None,
6342 };
6343 let s2 = ResponseSnapshot {
6344 status: 404,
6345 ..s1.clone()
6346 };
6347 let diff = s1.diff(&s2);
6348 assert!(diff.contains("200 vs 404"));
6349 }
6350
6351 #[test]
6352 fn snapshot_diff_body() {
6353 let s1 = ResponseSnapshot {
6354 status: 200,
6355 headers: vec![],
6356 body: "hello".to_string(),
6357 body_json: None,
6358 };
6359 let s2 = ResponseSnapshot {
6360 body: "world".to_string(),
6361 ..s1.clone()
6362 };
6363 let diff = s1.diff(&s2);
6364 assert!(diff.contains("Body:"));
6365 }
6366
6367 #[test]
6368 fn snapshot_diff_no_differences() {
6369 let s = ResponseSnapshot {
6370 status: 200,
6371 headers: vec![],
6372 body: "ok".to_string(),
6373 body_json: None,
6374 };
6375 assert_eq!(s.diff(&s), "No differences");
6376 }
6377
6378 #[test]
6379 fn snapshot_mask_fields() {
6380 let resp = mock_test_response(
6381 200,
6382 r#"{"id":42,"name":"Alice","created_at":"2026-01-01"}"#,
6383 &[],
6384 );
6385 let snap = ResponseSnapshot::from_test_response(&resp)
6386 .mask_fields(&["id", "created_at"], "<MASKED>");
6387
6388 let json = snap.body_json.unwrap();
6389 assert_eq!(json["id"], "<MASKED>");
6390 assert_eq!(json["name"], "Alice");
6391 assert_eq!(json["created_at"], "<MASKED>");
6392 }
6393
6394 #[test]
6395 fn snapshot_mask_nested_fields() {
6396 let resp = mock_test_response(200, r#"{"user":{"id":1,"name":"Bob"}}"#, &[]);
6397 let snap =
6398 ResponseSnapshot::from_test_response(&resp).mask_fields(&["user.id"], "<MASKED>");
6399
6400 let json = snap.body_json.unwrap();
6401 assert_eq!(json["user"]["id"], "<MASKED>");
6402 assert_eq!(json["user"]["name"], "Bob");
6403 }
6404
6405 #[test]
6406 fn snapshot_save_and_load() {
6407 let snap = ResponseSnapshot {
6408 status: 200,
6409 headers: vec![("content-type".to_string(), "application/json".to_string())],
6410 body: r#"{"ok":true}"#.to_string(),
6411 body_json: Some(serde_json::json!({"ok": true})),
6412 };
6413
6414 let dir = std::env::temp_dir().join("fastapi_snapshot_test");
6415 let path = dir.join("test_snap.json");
6416 snap.save(&path).unwrap();
6417
6418 let loaded = ResponseSnapshot::load(&path).unwrap();
6419 assert_eq!(snap, loaded);
6420
6421 let _ = std::fs::remove_dir_all(&dir);
6423 }
6424
6425 #[test]
6426 fn snapshot_matches_ignoring_headers() {
6427 let s1 = ResponseSnapshot {
6428 status: 200,
6429 headers: vec![
6430 ("content-type".to_string(), "application/json".to_string()),
6431 ("x-request-id".to_string(), "abc".to_string()),
6432 ],
6433 body: "ok".to_string(),
6434 body_json: None,
6435 };
6436 let s2 = ResponseSnapshot {
6437 headers: vec![
6438 ("content-type".to_string(), "application/json".to_string()),
6439 ("x-request-id".to_string(), "xyz".to_string()),
6440 ],
6441 ..s1.clone()
6442 };
6443
6444 assert!(!s1.matches_ignoring_headers(&s2, &[]));
6445 assert!(s1.matches_ignoring_headers(&s2, &["X-Request-Id"]));
6446 }
6447
6448 #[test]
6449 fn snapshot_with_selected_headers() {
6450 let resp = mock_test_response(
6451 200,
6452 "ok",
6453 &[
6454 ("content-type", "text/plain"),
6455 ("x-request-id", "abc123"),
6456 ("x-trace-id", "trace-456"),
6457 ],
6458 );
6459 let snap = ResponseSnapshot::from_test_response_with_headers(&resp, &["content-type"]);
6460
6461 assert_eq!(snap.headers.len(), 1);
6462 assert_eq!(snap.headers[0].0, "content-type");
6463 }
6464
6465 #[test]
6466 fn snapshot_json_structural_comparison() {
6467 let s1 = ResponseSnapshot {
6469 status: 200,
6470 headers: vec![],
6471 body: r#"{"a":1,"b":2}"#.to_string(),
6472 body_json: Some(serde_json::json!({"a": 1, "b": 2})),
6473 };
6474 let s2 = ResponseSnapshot {
6475 body: r#"{"b":2,"a":1}"#.to_string(),
6476 body_json: Some(serde_json::json!({"b": 2, "a": 1})),
6477 ..s1.clone()
6478 };
6479
6480 assert_ne!(s1, s2);
6482 assert!(s1.matches_ignoring_headers(&s2, &[]));
6484 }
6485}
6486
6487#[cfg(test)]
6488mod mock_server_tests {
6489 use super::*;
6490 use serial_test::serial;
6491
6492 #[test]
6493 #[serial(testing_network)]
6494 fn mock_server_starts_and_responds() {
6495 let server = MockServer::start();
6496 server.mock_response("/hello", MockResponse::ok().body_str("Hello, World!"));
6497
6498 let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6500 stream
6501 .write_all(b"GET /hello HTTP/1.1\r\nHost: localhost\r\n\r\n")
6502 .unwrap();
6503
6504 let mut response = String::new();
6505 stream.read_to_string(&mut response).unwrap();
6506
6507 assert!(response.contains("200 OK"));
6508 assert!(response.contains("Hello, World!"));
6509 }
6510
6511 #[test]
6512 #[serial(testing_network)]
6513 fn mock_server_records_requests() {
6514 let server = MockServer::start();
6515
6516 let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6518 stream
6519 .write_all(b"GET /api/users HTTP/1.1\r\nHost: localhost\r\nX-Custom: value\r\n\r\n")
6520 .unwrap();
6521 let mut response = Vec::new();
6522 let _ = stream.read_to_end(&mut response);
6523
6524 thread::sleep(Duration::from_millis(50));
6526
6527 let requests = server.requests();
6528 assert_eq!(requests.len(), 1);
6529 assert_eq!(requests[0].method, "GET");
6530 assert_eq!(requests[0].path, "/api/users");
6531 assert_eq!(requests[0].header("x-custom"), Some("value"));
6532 }
6533
6534 #[test]
6535 #[serial(testing_network)]
6536 fn mock_server_handles_post_with_body() {
6537 let server = MockServer::start();
6538 server.mock_response(
6539 "/api/create",
6540 MockResponse::with_status(201).body_str("Created"),
6541 );
6542
6543 let body = r#"{"name":"test"}"#;
6544 let request = format!(
6545 "POST /api/create HTTP/1.1\r\nHost: localhost\r\nContent-Length: {}\r\nContent-Type: application/json\r\n\r\n{}",
6546 body.len(),
6547 body
6548 );
6549
6550 let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6551 stream.write_all(request.as_bytes()).unwrap();
6552 let mut response = String::new();
6553 stream.read_to_string(&mut response).unwrap();
6554
6555 assert!(response.contains("201 Created"));
6556
6557 thread::sleep(Duration::from_millis(50));
6558 let requests = server.requests();
6559 assert_eq!(requests.len(), 1);
6560 assert_eq!(requests[0].method, "POST");
6561 assert_eq!(requests[0].body_text(), body);
6562 }
6563
6564 #[test]
6565 #[serial(testing_network)]
6566 fn mock_server_pattern_matching() {
6567 let server = MockServer::start();
6568 server.mock_response("/api/*", MockResponse::ok().body_str("API Response"));
6569
6570 let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6571 stream
6572 .write_all(b"GET /api/users/123 HTTP/1.1\r\nHost: localhost\r\n\r\n")
6573 .unwrap();
6574 let mut response = String::new();
6575 stream.read_to_string(&mut response).unwrap();
6576
6577 assert!(response.contains("API Response"));
6578 }
6579
6580 #[test]
6581 #[serial(testing_network)]
6582 fn mock_server_default_response() {
6583 let server = MockServer::start();
6584
6585 let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6586 stream
6587 .write_all(b"GET /unknown HTTP/1.1\r\nHost: localhost\r\n\r\n")
6588 .unwrap();
6589 let mut response = String::new();
6590 stream.read_to_string(&mut response).unwrap();
6591
6592 assert!(response.contains("404"));
6593 }
6594
6595 #[test]
6596 #[serial(testing_network)]
6597 fn mock_server_url_helpers() {
6598 let server = MockServer::start();
6599
6600 let url = server.url();
6601 assert!(url.starts_with("http://127.0.0.1:"));
6602
6603 let api_url = server.url_for("/api/users");
6604 assert!(api_url.contains("/api/users"));
6605 }
6606
6607 #[test]
6608 #[serial(testing_network)]
6609 fn mock_server_clear_requests() {
6610 let server = MockServer::start();
6611
6612 let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6614 stream
6615 .write_all(b"GET /test HTTP/1.1\r\nHost: localhost\r\n\r\n")
6616 .unwrap();
6617 let mut response = Vec::new();
6618 let _ = stream.read_to_end(&mut response);
6619
6620 thread::sleep(Duration::from_millis(50));
6621 assert_eq!(server.request_count(), 1);
6622
6623 server.clear_requests();
6624 assert_eq!(server.request_count(), 0);
6625 }
6626
6627 #[test]
6628 #[serial(testing_network)]
6629 fn mock_server_wait_for_requests() {
6630 let server = MockServer::start();
6631
6632 let addr = server.addr();
6634 thread::spawn(move || {
6635 thread::sleep(Duration::from_millis(50));
6636 let mut stream = StdTcpStream::connect(addr).expect("Failed to connect");
6637 stream
6638 .write_all(b"GET /delayed HTTP/1.1\r\nHost: localhost\r\n\r\n")
6639 .unwrap();
6640 });
6641
6642 let received = server.wait_for_requests(1, Duration::from_millis(500));
6643 assert!(received);
6644 assert_eq!(server.request_count(), 1);
6645 }
6646
6647 #[test]
6648 #[serial(testing_network)]
6649 fn mock_server_assert_helpers() {
6650 let server = MockServer::start();
6651
6652 let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6653 stream
6654 .write_all(b"GET /expected HTTP/1.1\r\nHost: localhost\r\n\r\n")
6655 .unwrap();
6656 let mut response = Vec::new();
6657 let _ = stream.read_to_end(&mut response);
6658
6659 thread::sleep(Duration::from_millis(50));
6660
6661 server.assert_received("/expected");
6662 server.assert_not_received("/not-expected");
6663 server.assert_request_count(1);
6664 }
6665
6666 #[test]
6667 #[serial(testing_network)]
6668 fn mock_server_query_string_parsing() {
6669 let server = MockServer::start();
6670
6671 let mut stream = StdTcpStream::connect(server.addr()).expect("Failed to connect");
6672 stream
6673 .write_all(b"GET /search?q=rust&limit=10 HTTP/1.1\r\nHost: localhost\r\n\r\n")
6674 .unwrap();
6675 let mut response = Vec::new();
6676 let _ = stream.read_to_end(&mut response);
6677
6678 thread::sleep(Duration::from_millis(50));
6679
6680 let requests = server.requests();
6681 assert_eq!(requests.len(), 1);
6682 assert_eq!(requests[0].path, "/search");
6683 assert_eq!(requests[0].query, Some("q=rust&limit=10".to_string()));
6684 assert_eq!(requests[0].url(), "/search?q=rust&limit=10");
6685 }
6686
6687 #[test]
6688 fn mock_response_json() {
6689 #[derive(serde::Serialize)]
6690 struct User {
6691 name: String,
6692 }
6693
6694 let response = MockResponse::ok().json(&User {
6695 name: "Alice".to_string(),
6696 });
6697 let bytes = response.to_http_response();
6698 let http = String::from_utf8_lossy(&bytes);
6699
6700 assert!(http.contains("application/json"));
6701 assert!(http.contains("Alice"));
6702 }
6703
6704 #[test]
6705 fn recorded_request_helpers() {
6706 let request = RecordedRequest {
6707 method: "GET".to_string(),
6708 path: "/api/users".to_string(),
6709 query: Some("page=1".to_string()),
6710 headers: vec![("Content-Type".to_string(), "application/json".to_string())],
6711 body: b"test body".to_vec(),
6712 timestamp: std::time::Instant::now(),
6713 };
6714
6715 assert_eq!(request.body_text(), "test body");
6716 assert_eq!(request.header("content-type"), Some("application/json"));
6717 assert_eq!(request.url(), "/api/users?page=1");
6718 }
6719}
6720
6721#[cfg(test)]
6722mod e2e_tests {
6723 use super::*;
6724
6725 fn test_handler(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
6727 let path = req.path();
6728 let response = match path {
6729 "/" => Response::ok().body(ResponseBody::Bytes(b"Home".to_vec())),
6730 "/login" => Response::ok().body(ResponseBody::Bytes(b"Login Page".to_vec())),
6731 "/dashboard" => Response::ok().body(ResponseBody::Bytes(b"Dashboard".to_vec())),
6732 "/api/users" => {
6733 Response::ok().body(ResponseBody::Bytes(b"[\"Alice\",\"Bob\"]".to_vec()))
6734 }
6735 "/fail" => Response::with_status(StatusCode::INTERNAL_SERVER_ERROR)
6736 .body(ResponseBody::Bytes(b"Error".to_vec())),
6737 _ => Response::with_status(StatusCode::NOT_FOUND)
6738 .body(ResponseBody::Bytes(b"Not Found".to_vec())),
6739 };
6740 std::future::ready(response)
6741 }
6742
6743 #[test]
6744 fn e2e_scenario_all_steps_pass() {
6745 let client = TestClient::new(test_handler);
6746 let mut scenario = E2EScenario::new("Basic Navigation", client);
6747
6748 scenario.step("Visit home page", |client| {
6749 let response = client.get("/").send();
6750 assert_eq!(response.status().as_u16(), 200);
6751 assert_eq!(response.text(), "Home");
6752 });
6753
6754 scenario.step("Visit login page", |client| {
6755 let response = client.get("/login").send();
6756 assert_eq!(response.status().as_u16(), 200);
6757 });
6758
6759 assert!(scenario.passed());
6760 assert_eq!(scenario.steps().len(), 2);
6761 assert!(scenario.steps().iter().all(|s| s.result.is_passed()));
6762 }
6763
6764 #[test]
6765 fn e2e_scenario_step_failure() {
6766 let client = TestClient::new(test_handler);
6767 let mut scenario = E2EScenario::new("Failure Test", client).stop_on_failure(true);
6768
6769 scenario.step("First step passes", |client| {
6770 let response = client.get("/").send();
6771 assert_eq!(response.status().as_u16(), 200);
6772 });
6773
6774 scenario.step("Second step fails", |_client| {
6775 panic!("Intentional failure");
6776 });
6777
6778 scenario.step("Third step skipped", |client| {
6779 let response = client.get("/dashboard").send();
6780 assert_eq!(response.status().as_u16(), 200);
6781 });
6782
6783 assert!(!scenario.passed());
6784 assert_eq!(scenario.steps().len(), 3);
6785 assert!(scenario.steps()[0].result.is_passed());
6786 assert!(scenario.steps()[1].result.is_failed());
6787 assert!(matches!(scenario.steps()[2].result, E2EStepResult::Skipped));
6788 }
6789
6790 #[test]
6791 fn e2e_scenario_continue_on_failure() {
6792 let client = TestClient::new(test_handler);
6793 let mut scenario = E2EScenario::new("Continue Test", client).stop_on_failure(false);
6794
6795 scenario.step("First step fails", |_client| {
6796 panic!("First failure");
6797 });
6798
6799 scenario.step("Second step still runs", |client| {
6800 let response = client.get("/").send();
6801 assert_eq!(response.status().as_u16(), 200);
6802 });
6803
6804 assert!(!scenario.passed());
6805 assert_eq!(scenario.steps().len(), 2);
6806 assert!(scenario.steps()[0].result.is_failed());
6807 assert!(scenario.steps()[1].result.is_passed());
6809 }
6810
6811 #[test]
6812 fn e2e_report_text_format() {
6813 let client = TestClient::new(test_handler);
6814 let mut scenario =
6815 E2EScenario::new("Report Test", client).description("Tests report generation");
6816
6817 scenario.step("Step 1", |client| {
6818 let _ = client.get("/").send();
6819 });
6820
6821 let report = scenario.report();
6822 let text = report.to_text();
6823
6824 assert!(text.contains("E2E Test Report: Report Test"));
6825 assert!(text.contains("Tests report generation"));
6826 assert!(text.contains("1 passed"));
6827 assert!(text.contains("Step 1"));
6828 }
6829
6830 #[test]
6831 fn e2e_report_json_format() {
6832 let client = TestClient::new(test_handler);
6833 let mut scenario = E2EScenario::new("JSON Test", client);
6834
6835 scenario.step("API call", |client| {
6836 let response = client.get("/api/users").send();
6837 assert_eq!(response.status().as_u16(), 200);
6838 });
6839
6840 let report = scenario.report();
6841 let json = report.to_json();
6842
6843 assert!(json.contains(r#""scenario": "JSON Test""#));
6844 assert!(json.contains(r#""passed": 1"#));
6845 assert!(json.contains(r#""name": "API call""#));
6846 assert!(json.contains(r#""status": "passed""#));
6847 }
6848
6849 #[test]
6850 fn e2e_report_html_format() {
6851 let client = TestClient::new(test_handler);
6852 let mut scenario = E2EScenario::new("HTML Test", client);
6853
6854 scenario.step("Web visit", |client| {
6855 let _ = client.get("/").send();
6856 });
6857
6858 let report = scenario.report();
6859 let html = report.to_html();
6860
6861 assert!(html.contains("<!DOCTYPE html>"));
6862 assert!(html.contains("E2E Report: HTML Test"));
6863 assert!(html.contains("1 passed"));
6864 assert!(html.contains("Web visit"));
6865 }
6866
6867 #[test]
6868 fn e2e_step_timing() {
6869 let client = TestClient::new(test_handler);
6870 let mut scenario = E2EScenario::new("Timing Test", client);
6871
6872 scenario.step("Timed step", |_client| {
6873 std::thread::sleep(std::time::Duration::from_millis(10));
6875 });
6876
6877 assert!(scenario.steps()[0].duration >= std::time::Duration::from_millis(10));
6878 }
6879
6880 #[test]
6881 fn e2e_logs_captured() {
6882 let client = TestClient::new(test_handler);
6883 let mut scenario = E2EScenario::new("Log Test", client);
6884
6885 scenario.log("Manual log entry");
6886 scenario.step("Logged step", |_client| {});
6887
6888 assert!(
6889 scenario
6890 .logs()
6891 .iter()
6892 .any(|l| l.contains("Manual log entry"))
6893 );
6894 assert!(
6895 scenario
6896 .logs()
6897 .iter()
6898 .any(|l| l.contains("[START] Logged step"))
6899 );
6900 assert!(
6901 scenario
6902 .logs()
6903 .iter()
6904 .any(|l| l.contains("[PASS] Logged step"))
6905 );
6906 }
6907
6908 #[test]
6909 fn e2e_try_step_with_result() {
6910 let client = TestClient::new(test_handler);
6911 let mut scenario = E2EScenario::new("Try Step Test", client);
6912
6913 let result: Result<(), &str> = scenario.try_step("Success step", |client| {
6914 let response = client.get("/").send();
6915 if response.status().as_u16() == 200 {
6916 Ok(())
6917 } else {
6918 Err("Unexpected status")
6919 }
6920 });
6921
6922 assert!(result.is_ok());
6923 assert!(scenario.passed());
6924 }
6925
6926 #[test]
6927 fn e2e_escape_functions() {
6928 assert_eq!(escape_json("hello"), "hello");
6930 assert_eq!(escape_json("a\"b"), "a\\\"b");
6931 assert_eq!(escape_json("a\nb"), "a\\nb");
6932
6933 assert_eq!(escape_html("hello"), "hello");
6935 assert_eq!(escape_html("<script>"), "<script>");
6936 assert_eq!(escape_html("a&b"), "a&b");
6937 }
6938
6939 #[test]
6940 fn e2e_step_result_helpers() {
6941 let passed = E2EStepResult::Passed;
6942 let failed = E2EStepResult::Failed("error".to_string());
6943 let skipped = E2EStepResult::Skipped;
6944
6945 assert!(passed.is_passed());
6946 assert!(!passed.is_failed());
6947
6948 assert!(!failed.is_passed());
6949 assert!(failed.is_failed());
6950
6951 assert!(!skipped.is_passed());
6952 assert!(!skipped.is_failed());
6953 }
6954}
6955
6956pub trait TestFixture: Sized + Send {
6990 fn setup() -> Self;
6992
6993 fn teardown(&mut self) {}
6997}
6998
6999pub struct FixtureGuard<F: TestFixture> {
7003 fixture: Option<F>,
7004}
7005
7006impl<F: TestFixture> FixtureGuard<F> {
7007 pub fn new() -> Self {
7009 Self {
7010 fixture: Some(F::setup()),
7011 }
7012 }
7013
7014 pub fn get(&self) -> &F {
7016 self.fixture.as_ref().unwrap()
7017 }
7018
7019 pub fn get_mut(&mut self) -> &mut F {
7021 self.fixture.as_mut().unwrap()
7022 }
7023}
7024
7025impl<F: TestFixture> Default for FixtureGuard<F> {
7026 fn default() -> Self {
7027 Self::new()
7028 }
7029}
7030
7031impl<F: TestFixture> Drop for FixtureGuard<F> {
7032 fn drop(&mut self) {
7033 if let Some(mut fixture) = self.fixture.take() {
7034 fixture.teardown();
7035 }
7036 }
7037}
7038
7039impl<F: TestFixture> std::ops::Deref for FixtureGuard<F> {
7040 type Target = F;
7041
7042 fn deref(&self) -> &Self::Target {
7043 self.get()
7044 }
7045}
7046
7047impl<F: TestFixture> std::ops::DerefMut for FixtureGuard<F> {
7048 fn deref_mut(&mut self) -> &mut Self::Target {
7049 self.get_mut()
7050 }
7051}
7052
7053pub struct IntegrationTest<H: Handler + 'static> {
7099 name: String,
7101 client: TestClient<H>,
7103 fixtures: HashMap<std::any::TypeId, Box<dyn std::any::Any + Send>>,
7105 reset_hooks: Vec<Box<dyn Fn() + Send + Sync>>,
7107}
7108
7109impl<H: Handler + 'static> IntegrationTest<H> {
7110 pub fn new(name: impl Into<String>, handler: H) -> Self {
7112 Self {
7113 name: name.into(),
7114 client: TestClient::new(handler),
7115 fixtures: HashMap::new(),
7116 reset_hooks: Vec::new(),
7117 }
7118 }
7119
7120 pub fn with_seed(name: impl Into<String>, handler: H, seed: u64) -> Self {
7122 Self {
7123 name: name.into(),
7124 client: TestClient::with_seed(handler, seed),
7125 fixtures: HashMap::new(),
7126 reset_hooks: Vec::new(),
7127 }
7128 }
7129
7130 #[must_use]
7134 pub fn with_fixture<F: TestFixture + 'static>(mut self) -> Self {
7135 let guard = FixtureGuard::<F>::new();
7136 self.fixtures
7137 .insert(std::any::TypeId::of::<F>(), Box::new(guard));
7138 self
7139 }
7140
7141 #[must_use]
7145 pub fn on_reset<F: Fn() + Send + Sync + 'static>(mut self, f: F) -> Self {
7146 self.reset_hooks.push(Box::new(f));
7147 self
7148 }
7149
7150 pub fn run<F>(mut self, test_fn: F)
7155 where
7156 F: FnOnce(&IntegrationTestContext<'_, H>) + std::panic::UnwindSafe,
7157 {
7158 let ctx = IntegrationTestContext {
7160 name: &self.name,
7161 client: &self.client,
7162 fixtures: &self.fixtures,
7163 };
7164
7165 let ctx_ref = std::panic::AssertUnwindSafe(&ctx);
7167
7168 let result = std::panic::catch_unwind(|| {
7170 test_fn(&ctx_ref);
7171 });
7172
7173 for hook in &self.reset_hooks {
7175 hook();
7176 }
7177
7178 self.client.clear_cookies();
7180 self.client.clear_dependency_overrides();
7181
7182 self.fixtures.clear();
7184
7185 if let Err(e) = result {
7187 std::panic::resume_unwind(e);
7188 }
7189 }
7190}
7191
7192pub struct IntegrationTestContext<'a, H: Handler> {
7194 name: &'a str,
7196 client: &'a TestClient<H>,
7198 fixtures: &'a HashMap<std::any::TypeId, Box<dyn std::any::Any + Send>>,
7200}
7201
7202impl<'a, H: Handler + 'static> IntegrationTestContext<'a, H> {
7203 #[must_use]
7205 pub fn name(&self) -> &str {
7206 self.name
7207 }
7208
7209 #[must_use]
7211 pub fn client(&self) -> &TestClient<H> {
7212 self.client
7213 }
7214
7215 #[must_use]
7219 pub fn fixture<F: TestFixture + 'static>(&self) -> Option<&F> {
7220 self.fixtures
7221 .get(&std::any::TypeId::of::<F>())
7222 .and_then(|boxed| boxed.downcast_ref::<FixtureGuard<F>>())
7223 .map(FixtureGuard::get)
7224 }
7225
7226 #[must_use]
7230 pub fn fixture_mut<F: TestFixture + 'static>(&self) -> Option<&mut F> {
7231 None }
7236
7237 pub fn get(&self, path: &str) -> RequestBuilder<'_, H> {
7241 self.client.get(path)
7242 }
7243
7244 pub fn post(&self, path: &str) -> RequestBuilder<'_, H> {
7246 self.client.post(path)
7247 }
7248
7249 pub fn put(&self, path: &str) -> RequestBuilder<'_, H> {
7251 self.client.put(path)
7252 }
7253
7254 pub fn delete(&self, path: &str) -> RequestBuilder<'_, H> {
7256 self.client.delete(path)
7257 }
7258
7259 pub fn patch(&self, path: &str) -> RequestBuilder<'_, H> {
7261 self.client.patch(path)
7262 }
7263
7264 pub fn options(&self, path: &str) -> RequestBuilder<'_, H> {
7266 self.client.options(path)
7267 }
7268
7269 pub fn request(&self, method: Method, path: &str) -> RequestBuilder<'_, H> {
7271 self.client.request(method, path)
7272 }
7273}
7274
7275#[cfg(test)]
7280mod test_server_tests {
7281 use super::*;
7282 use crate::app::App;
7283 use serial_test::serial;
7284 use std::net::TcpStream as StdTcpStreamAlias;
7285
7286 fn make_test_app() -> App {
7287 App::builder()
7288 .get("/health", |_ctx: &RequestContext, _req: &mut Request| {
7289 std::future::ready(
7290 Response::ok()
7291 .header("content-type", b"text/plain".to_vec())
7292 .body(ResponseBody::Bytes(b"OK".to_vec())),
7293 )
7294 })
7295 .get("/hello", |_ctx: &RequestContext, _req: &mut Request| {
7296 std::future::ready(
7297 Response::ok()
7298 .header("content-type", b"application/json".to_vec())
7299 .body(ResponseBody::Bytes(
7300 br#"{"message":"Hello, World!"}"#.to_vec(),
7301 )),
7302 )
7303 })
7304 .post("/echo", |_ctx: &RequestContext, req: &mut Request| {
7305 let body = match req.body() {
7306 Body::Bytes(b) => b.clone(),
7307 _ => Vec::new(),
7308 };
7309 std::future::ready(
7310 Response::ok()
7311 .header("content-type", b"application/octet-stream".to_vec())
7312 .body(ResponseBody::Bytes(body)),
7313 )
7314 })
7315 .build()
7316 }
7317
7318 fn send_request(addr: SocketAddr, request: &[u8]) -> String {
7319 let mut stream = StdTcpStreamAlias::connect(addr).expect("Failed to connect to TestServer");
7320 stream
7321 .set_read_timeout(Some(Duration::from_secs(10)))
7322 .expect("set_read_timeout");
7323 stream.write_all(request).expect("Failed to write request");
7324 stream.flush().expect("Failed to flush");
7325
7326 let mut out = Vec::with_capacity(4096);
7331 let mut buf = [0u8; 8192];
7332 loop {
7333 match stream.read(&mut buf) {
7334 Ok(0) => break,
7335 Ok(n) => out.extend_from_slice(&buf[..n]),
7336 Err(err) if err.kind() == std::io::ErrorKind::ConnectionReset => break,
7337 Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => break,
7338 Err(err) => panic!("Failed to read response: {err:?}"),
7339 }
7340 }
7341 String::from_utf8_lossy(&out).to_string()
7342 }
7343
7344 #[test]
7345 #[serial(testing_network)]
7346 fn test_server_starts_and_responds() {
7347 let app = make_test_app();
7348 let server = TestServer::start(app);
7349
7350 let response = send_request(
7351 server.addr(),
7352 b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7353 );
7354
7355 assert!(
7356 response.contains("200 OK"),
7357 "Expected 200 OK, got: {response}"
7358 );
7359 assert!(response.contains("OK"), "Expected body 'OK'");
7360 }
7361
7362 #[test]
7363 #[serial(testing_network)]
7364 fn test_server_json_response() {
7365 let app = make_test_app();
7366 let server = TestServer::start(app);
7367
7368 let response = send_request(
7369 server.addr(),
7370 b"GET /hello HTTP/1.1\r\nHost: localhost\r\n\r\n",
7371 );
7372
7373 assert!(response.contains("200 OK"));
7374 assert!(response.contains("application/json"));
7375 assert!(response.contains(r#"{"message":"Hello, World!"}"#));
7376 }
7377
7378 #[test]
7379 #[serial(testing_network)]
7380 fn test_server_post_with_body() {
7381 let app = make_test_app();
7382 let server = TestServer::start(app);
7383
7384 let request =
7385 b"POST /echo HTTP/1.1\r\nHost: localhost\r\nContent-Length: 11\r\n\r\nHello World";
7386 let response = send_request(server.addr(), request);
7387
7388 assert!(response.contains("200 OK"));
7389 assert!(response.contains("Hello World"));
7390 }
7391
7392 #[test]
7393 #[serial(testing_network)]
7394 fn test_server_logs_requests() {
7395 let app = make_test_app();
7396 let server = TestServer::start(app);
7397
7398 send_request(
7400 server.addr(),
7401 b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7402 );
7403
7404 let logs = server.log_entries();
7405 assert_eq!(logs.len(), 1);
7406 assert_eq!(logs[0].method, "GET");
7407 assert_eq!(logs[0].path, "/health");
7408 assert_eq!(logs[0].status, 200);
7409 }
7410
7411 #[test]
7412 #[serial(testing_network)]
7413 fn test_server_request_count() {
7414 let app = make_test_app();
7415 let server = TestServer::start(app);
7416
7417 assert_eq!(server.request_count(), 0);
7418
7419 send_request(
7420 server.addr(),
7421 b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7422 );
7423 send_request(
7424 server.addr(),
7425 b"GET /hello HTTP/1.1\r\nHost: localhost\r\n\r\n",
7426 );
7427
7428 assert_eq!(server.request_count(), 2);
7429 }
7430
7431 #[test]
7432 #[serial(testing_network)]
7433 fn test_server_clear_logs() {
7434 let app = make_test_app();
7435 let server = TestServer::start(app);
7436
7437 send_request(
7438 server.addr(),
7439 b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7440 );
7441 assert_eq!(server.request_count(), 1);
7442
7443 server.clear_logs();
7444 assert_eq!(server.request_count(), 0);
7445 }
7446
7447 #[test]
7448 #[serial(testing_network)]
7449 fn test_server_url_helpers() {
7450 let app = make_test_app();
7451 let server = TestServer::start(app);
7452
7453 assert!(server.url().starts_with("http://127.0.0.1:"));
7454 assert!(server.url_for("/health").ends_with("/health"));
7455 assert!(server.url_for("health").ends_with("/health"));
7456 assert!(server.port() > 0);
7457 }
7458
7459 #[test]
7460 #[serial(testing_network)]
7461 fn test_server_shutdown() {
7462 let app = make_test_app();
7463 let server = TestServer::start(app);
7464 let addr = server.addr();
7465
7466 let response = send_request(addr, b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n");
7468 assert!(response.contains("200 OK"));
7469
7470 server.shutdown();
7472 assert!(server.is_shutdown());
7473 }
7474
7475 #[test]
7476 #[serial(testing_network)]
7477 fn test_server_config_no_logging() {
7478 let app = make_test_app();
7479 let config = TestServerConfig::new().log_requests(false);
7480 let server = TestServer::start_with_config(app, config);
7481
7482 send_request(
7483 server.addr(),
7484 b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7485 );
7486
7487 assert_eq!(server.request_count(), 0);
7489 }
7490
7491 #[test]
7492 #[serial(testing_network)]
7493 fn test_server_bad_request() {
7494 let app = make_test_app();
7495 let server = TestServer::start(app);
7496
7497 let response = send_request(server.addr(), b"NOT_HTTP_AT_ALL");
7499
7500 assert!(response.contains("400 Bad Request"));
7501 }
7502
7503 #[test]
7504 #[serial(testing_network)]
7505 fn test_server_content_length_header() {
7506 let app = make_test_app();
7507 let server = TestServer::start(app);
7508
7509 let response = send_request(
7510 server.addr(),
7511 b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7512 );
7513
7514 assert!(
7516 response.contains("content-length: 2"),
7517 "Expected content-length: 2, got: {response}"
7518 );
7519 }
7520
7521 #[test]
7522 #[serial(testing_network)]
7523 fn test_server_multiple_requests_sequential() {
7524 let app = make_test_app();
7525 let server = TestServer::start(app);
7526
7527 for _ in 0..5 {
7528 let response = send_request(
7529 server.addr(),
7530 b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7531 );
7532 assert!(response.contains("200 OK"));
7533 }
7534
7535 assert_eq!(server.request_count(), 5);
7536 }
7537
7538 #[test]
7539 #[serial(testing_network)]
7540 fn test_server_log_entry_has_timing() {
7541 let app = make_test_app();
7542 let server = TestServer::start(app);
7543
7544 send_request(
7545 server.addr(),
7546 b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7547 );
7548
7549 let logs = server.log_entries();
7550 assert_eq!(logs.len(), 1);
7551 assert!(logs[0].duration < Duration::from_secs(1));
7553 }
7554
7555 #[test]
7560 #[serial(testing_network)]
7561 fn test_server_shutdown_controller_available() {
7562 let app = make_test_app();
7563 let server = TestServer::start(app);
7564
7565 let controller = server.shutdown_controller();
7567 assert!(!controller.is_shutting_down());
7568 assert_eq!(controller.phase(), crate::shutdown::ShutdownPhase::Running);
7569 }
7570
7571 #[test]
7572 #[serial(testing_network)]
7573 fn test_server_shutdown_triggers_controller() {
7574 let app = make_test_app();
7575 let server = TestServer::start(app);
7576
7577 assert!(!server.shutdown_controller().is_shutting_down());
7579
7580 server.shutdown();
7582
7583 assert!(server.is_shutdown());
7585 assert!(server.shutdown_controller().is_shutting_down());
7586 assert_eq!(
7587 server.shutdown_controller().phase(),
7588 crate::shutdown::ShutdownPhase::StopAccepting
7589 );
7590 }
7591
7592 #[test]
7593 #[serial(testing_network)]
7594 fn test_server_requests_complete_before_shutdown() {
7595 let app = make_test_app();
7596 let server = TestServer::start(app);
7597
7598 let response = send_request(
7600 server.addr(),
7601 b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7602 );
7603 assert!(response.contains("200 OK"));
7604 assert_eq!(server.request_count(), 1);
7605
7606 server.shutdown();
7608
7609 let logs = server.log_entries();
7611 assert_eq!(logs.len(), 1);
7612 assert_eq!(logs[0].status, 200);
7613 assert_eq!(logs[0].path, "/health");
7614 }
7615
7616 #[test]
7617 #[serial(testing_network)]
7618 fn test_server_in_flight_tracking() {
7619 let app = make_test_app();
7620 let server = TestServer::start(app);
7621
7622 assert_eq!(server.in_flight_count(), 0);
7624
7625 send_request(
7628 server.addr(),
7629 b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7630 );
7631
7632 let start = std::time::Instant::now();
7636 let timeout = std::time::Duration::from_millis(500);
7637 while server.in_flight_count() > 0 && start.elapsed() < timeout {
7638 std::thread::sleep(std::time::Duration::from_millis(1));
7639 }
7640 assert_eq!(
7641 server.in_flight_count(),
7642 0,
7643 "In-flight count should return to 0 after request completes"
7644 );
7645 }
7646
7647 #[test]
7648 #[serial(testing_network)]
7649 fn test_server_in_flight_guard_tracks_correctly() {
7650 let app = make_test_app();
7651 let server = TestServer::start(app);
7652
7653 let controller = server.shutdown_controller();
7655 assert_eq!(controller.in_flight_count(), 0);
7656
7657 let guard1 = controller.track_request();
7658 assert_eq!(controller.in_flight_count(), 1);
7659
7660 let guard2 = controller.track_request();
7661 assert_eq!(controller.in_flight_count(), 2);
7662
7663 drop(guard1);
7664 assert_eq!(controller.in_flight_count(), 1);
7665
7666 drop(guard2);
7667 assert_eq!(controller.in_flight_count(), 0);
7668 }
7669
7670 #[test]
7671 #[serial(testing_network)]
7672 fn test_server_shutdown_hooks_executed() {
7673 let app = make_test_app();
7674 let server = TestServer::start(app);
7675
7676 let hook_executed = Arc::new(AtomicBool::new(false));
7678 let hook_executed_clone = Arc::clone(&hook_executed);
7679 server.shutdown_controller().register_hook(move || {
7680 hook_executed_clone.store(true, std::sync::atomic::Ordering::Release);
7681 });
7682
7683 assert!(!hook_executed.load(std::sync::atomic::Ordering::Acquire));
7684
7685 server.shutdown();
7687
7688 drop(server);
7691
7692 assert!(
7693 hook_executed.load(std::sync::atomic::Ordering::Acquire),
7694 "Shutdown hook should have been executed"
7695 );
7696 }
7697
7698 #[test]
7699 #[serial(testing_network)]
7700 fn test_server_multiple_shutdown_hooks_lifo() {
7701 let app = make_test_app();
7702 let server = TestServer::start(app);
7703
7704 let execution_order = Arc::new(Mutex::new(Vec::new()));
7705
7706 let order1 = Arc::clone(&execution_order);
7707 server.shutdown_controller().register_hook(move || {
7708 order1.lock().push(1);
7709 });
7710
7711 let order2 = Arc::clone(&execution_order);
7712 server.shutdown_controller().register_hook(move || {
7713 order2.lock().push(2);
7714 });
7715
7716 let order3 = Arc::clone(&execution_order);
7717 server.shutdown_controller().register_hook(move || {
7718 order3.lock().push(3);
7719 });
7720
7721 server.shutdown();
7723 drop(server);
7724
7725 let order = execution_order.lock();
7727 assert_eq!(*order, vec![3, 2, 1]);
7728 }
7729
7730 #[test]
7731 #[serial(testing_network)]
7732 fn test_server_shutdown_controller_phase_progression() {
7733 let app = make_test_app();
7734 let server = TestServer::start(app);
7735
7736 let controller = server.shutdown_controller();
7737 assert_eq!(controller.phase(), crate::shutdown::ShutdownPhase::Running);
7738
7739 assert!(controller.advance_phase());
7741 assert_eq!(
7742 controller.phase(),
7743 crate::shutdown::ShutdownPhase::StopAccepting
7744 );
7745
7746 assert!(controller.advance_phase());
7747 assert_eq!(
7748 controller.phase(),
7749 crate::shutdown::ShutdownPhase::ShutdownFlagged
7750 );
7751
7752 assert!(controller.advance_phase());
7753 assert_eq!(
7754 controller.phase(),
7755 crate::shutdown::ShutdownPhase::GracePeriod
7756 );
7757
7758 assert!(controller.advance_phase());
7759 assert_eq!(
7760 controller.phase(),
7761 crate::shutdown::ShutdownPhase::Cancelling
7762 );
7763
7764 assert!(controller.advance_phase());
7765 assert_eq!(
7766 controller.phase(),
7767 crate::shutdown::ShutdownPhase::RunningHooks
7768 );
7769
7770 assert!(controller.advance_phase());
7771 assert_eq!(controller.phase(), crate::shutdown::ShutdownPhase::Stopped);
7772
7773 assert!(!controller.advance_phase());
7775 }
7776
7777 #[test]
7778 #[serial(testing_network)]
7779 fn test_server_receiver_notified_on_shutdown() {
7780 let app = make_test_app();
7781 let server = TestServer::start(app);
7782
7783 let receiver = server.shutdown_controller().subscribe();
7784 assert!(!receiver.is_shutting_down());
7785
7786 server.shutdown();
7787 assert!(receiver.is_shutting_down());
7788 assert!(!receiver.is_forced());
7789 }
7790
7791 #[test]
7792 #[serial(testing_network)]
7793 fn test_server_forced_shutdown() {
7794 let app = make_test_app();
7795 let server = TestServer::start(app);
7796
7797 let receiver = server.shutdown_controller().subscribe();
7798
7799 server.shutdown_controller().shutdown();
7801 assert!(receiver.is_shutting_down());
7802 assert!(!receiver.is_forced());
7803
7804 server.shutdown_controller().shutdown();
7806 assert!(receiver.is_forced());
7807 }
7808
7809 #[test]
7810 #[serial(testing_network)]
7811 fn test_server_requests_work_before_shutdown_signal() {
7812 let app = make_test_app();
7813 let server = TestServer::start(app);
7814
7815 for i in 0..3 {
7817 let response = send_request(
7818 server.addr(),
7819 b"GET /health HTTP/1.1\r\nHost: localhost\r\n\r\n",
7820 );
7821 assert!(
7822 response.contains("200 OK"),
7823 "Request {i} should succeed before shutdown"
7824 );
7825 }
7826
7827 assert_eq!(server.request_count(), 3);
7828
7829 server.shutdown();
7831 assert!(server.is_shutdown());
7832 }
7833}