Skip to main content

reinhardt_testkit/server_fn/
mock_request.rs

1//! Mock HTTP request and response utilities for server function testing.
2//!
3//! This module provides mock HTTP request/response types that can be used
4//! to simulate HTTP interactions in server function tests.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use reinhardt_testkit::server_fn::mock_request::{MockHttpRequest, MockHttpResponse};
10//!
11//! let request = MockHttpRequest::post("/api/users")
12//!     .with_json(&CreateUserInput { name: "Alice".to_string() })
13//!     .with_header("Authorization", "Bearer token");
14//!
15//! // Use request in server function testing
16//! ```
17
18#![cfg(native)]
19
20use std::collections::HashMap;
21
22use bytes::Bytes;
23use http::{HeaderMap, HeaderValue, Method, StatusCode, Uri, header::HeaderName};
24use serde::{Deserialize, Serialize};
25
26/// Mock HTTP request for testing server functions.
27///
28/// This provides a fluent API for building HTTP requests without an actual
29/// HTTP layer, useful for testing server functions directly.
30#[derive(Debug, Clone)]
31pub struct MockHttpRequest {
32	/// The HTTP method.
33	pub method: Method,
34	/// The request URI.
35	pub uri: Uri,
36	/// Request headers.
37	pub headers: HeaderMap,
38	/// Request body as bytes.
39	pub body: Bytes,
40	/// Cookies extracted from headers.
41	pub cookies: HashMap<String, String>,
42	/// Query parameters.
43	pub query_params: HashMap<String, String>,
44}
45
46impl Default for MockHttpRequest {
47	fn default() -> Self {
48		Self {
49			method: Method::GET,
50			uri: "/".parse().unwrap(),
51			headers: HeaderMap::new(),
52			body: Bytes::new(),
53			cookies: HashMap::new(),
54			query_params: HashMap::new(),
55		}
56	}
57}
58
59impl MockHttpRequest {
60	/// Create a new mock request with the given method and URI.
61	pub fn new(method: Method, uri: &str) -> Self {
62		let parsed_uri: Uri = uri.parse().unwrap_or_else(|_| "/".parse().unwrap());
63
64		// Extract query parameters
65		let query_params = parsed_uri
66			.query()
67			.map(|q| {
68				url::form_urlencoded::parse(q.as_bytes())
69					.map(|(k, v)| (k.to_string(), v.to_string()))
70					.collect()
71			})
72			.unwrap_or_default();
73
74		Self {
75			method,
76			uri: parsed_uri,
77			query_params,
78			..Default::default()
79		}
80	}
81
82	/// Create a GET request.
83	pub fn get(uri: &str) -> Self {
84		Self::new(Method::GET, uri)
85	}
86
87	/// Create a POST request.
88	pub fn post(uri: &str) -> Self {
89		Self::new(Method::POST, uri)
90	}
91
92	/// Create a PUT request.
93	pub fn put(uri: &str) -> Self {
94		Self::new(Method::PUT, uri)
95	}
96
97	/// Create a PATCH request.
98	pub fn patch(uri: &str) -> Self {
99		Self::new(Method::PATCH, uri)
100	}
101
102	/// Create a DELETE request.
103	pub fn delete(uri: &str) -> Self {
104		Self::new(Method::DELETE, uri)
105	}
106
107	/// Set the request body as JSON.
108	///
109	/// This serializes the value to JSON and sets the appropriate Content-Type header.
110	///
111	/// # Panics
112	///
113	/// Panics if the value cannot be serialized to JSON. Since this is a test
114	/// utility, invalid input indicates a test setup error that should be
115	/// caught immediately.
116	// Fixes #876
117	pub fn with_json<T: Serialize>(mut self, body: &T) -> Self {
118		let bytes = serde_json::to_vec(body).unwrap_or_else(|err| {
119			panic!("MockHttpRequest::with_json: failed to serialize body to JSON: {err}")
120		});
121		self.body = Bytes::from(bytes);
122		self.headers.insert(
123			http::header::CONTENT_TYPE,
124			HeaderValue::from_static("application/json"),
125		);
126		self
127	}
128
129	/// Set the request body as form data.
130	///
131	/// This serializes the value as URL-encoded form data.
132	///
133	/// # Panics
134	///
135	/// Panics if the value cannot be serialized as form data. Since this is a
136	/// test utility, invalid input indicates a test setup error that should be
137	/// caught immediately.
138	// Fixes #876
139	pub fn with_form<T: Serialize>(mut self, body: &T) -> Self {
140		let encoded = serde_urlencoded::to_string(body).unwrap_or_else(|err| {
141			panic!("MockHttpRequest::with_form: failed to serialize body as form data: {err}")
142		});
143		self.body = Bytes::from(encoded);
144		self.headers.insert(
145			http::header::CONTENT_TYPE,
146			HeaderValue::from_static("application/x-www-form-urlencoded"),
147		);
148		self
149	}
150
151	/// Set the request body as raw bytes.
152	pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
153		self.body = body.into();
154		self
155	}
156
157	/// Set the request body as a string.
158	pub fn with_text(mut self, body: impl Into<String>) -> Self {
159		self.body = Bytes::from(body.into());
160		self.headers.insert(
161			http::header::CONTENT_TYPE,
162			HeaderValue::from_static("text/plain"),
163		);
164		self
165	}
166
167	/// Add a request header.
168	pub fn with_header(mut self, name: &str, value: &str) -> Self {
169		if let (Ok(header_name), Ok(header_value)) = (
170			HeaderName::from_bytes(name.as_bytes()),
171			HeaderValue::from_str(value),
172		) {
173			self.headers.insert(header_name, header_value);
174		}
175		self
176	}
177
178	/// Add multiple headers.
179	pub fn with_headers<'a>(
180		mut self,
181		headers: impl IntoIterator<Item = (&'a str, &'a str)>,
182	) -> Self {
183		for (name, value) in headers {
184			if let (Ok(header_name), Ok(header_value)) = (
185				HeaderName::from_bytes(name.as_bytes()),
186				HeaderValue::from_str(value),
187			) {
188				self.headers.insert(header_name, header_value);
189			}
190		}
191		self
192	}
193
194	/// Add a cookie.
195	pub fn with_cookie(mut self, name: &str, value: &str) -> Self {
196		self.cookies.insert(name.to_string(), value.to_string());
197		self.update_cookie_header();
198		self
199	}
200
201	/// Add multiple cookies.
202	pub fn with_cookies<'a>(
203		mut self,
204		cookies: impl IntoIterator<Item = (&'a str, &'a str)>,
205	) -> Self {
206		for (name, value) in cookies {
207			self.cookies.insert(name.to_string(), value.to_string());
208		}
209		self.update_cookie_header();
210		self
211	}
212
213	/// Add a query parameter.
214	pub fn with_query(mut self, name: &str, value: &str) -> Self {
215		self.query_params
216			.insert(name.to_string(), value.to_string());
217		self.update_uri_query();
218		self
219	}
220
221	/// Add multiple query parameters.
222	pub fn with_query_params<'a>(
223		mut self,
224		params: impl IntoIterator<Item = (&'a str, &'a str)>,
225	) -> Self {
226		for (name, value) in params {
227			self.query_params
228				.insert(name.to_string(), value.to_string());
229		}
230		self.update_uri_query();
231		self
232	}
233
234	/// Set the Authorization header with a Bearer token.
235	pub fn with_bearer_token(self, token: &str) -> Self {
236		self.with_header("Authorization", &format!("Bearer {}", token))
237	}
238
239	/// Set the Authorization header with Basic auth.
240	pub fn with_basic_auth(self, username: &str, password: &str) -> Self {
241		let credentials =
242			base64_simd::STANDARD.encode_to_string(format!("{}:{}", username, password));
243		self.with_header("Authorization", &format!("Basic {}", credentials))
244	}
245
246	/// Set the Content-Type header.
247	pub fn with_content_type(self, content_type: &str) -> Self {
248		self.with_header("Content-Type", content_type)
249	}
250
251	/// Set the Accept header.
252	pub fn with_accept(self, accept: &str) -> Self {
253		self.with_header("Accept", accept)
254	}
255
256	/// Get the request path (without query string).
257	pub fn path(&self) -> &str {
258		self.uri.path()
259	}
260
261	/// Get the full URI as a string.
262	pub fn uri_string(&self) -> String {
263		self.uri.to_string()
264	}
265
266	/// Get a header value.
267	pub fn get_header(&self, name: &str) -> Option<&str> {
268		self.headers.get(name).and_then(|v| v.to_str().ok())
269	}
270
271	/// Get a cookie value.
272	pub fn get_cookie(&self, name: &str) -> Option<&str> {
273		self.cookies.get(name).map(|s| s.as_str())
274	}
275
276	/// Get a query parameter value.
277	pub fn get_query(&self, name: &str) -> Option<&str> {
278		self.query_params.get(name).map(|s| s.as_str())
279	}
280
281	/// Parse the body as JSON.
282	pub fn json<T: for<'de> Deserialize<'de>>(&self) -> Result<T, serde_json::Error> {
283		serde_json::from_slice(&self.body)
284	}
285
286	/// Parse the body as form data.
287	pub fn form<T: for<'de> Deserialize<'de>>(&self) -> Result<T, serde_urlencoded::de::Error> {
288		serde_urlencoded::from_bytes(&self.body)
289	}
290
291	/// Get the body as a string.
292	pub fn text(&self) -> Result<String, std::string::FromUtf8Error> {
293		String::from_utf8(self.body.to_vec())
294	}
295
296	fn update_cookie_header(&mut self) {
297		if self.cookies.is_empty() {
298			self.headers.remove(http::header::COOKIE);
299		} else {
300			let cookie_str: String = self
301				.cookies
302				.iter()
303				.map(|(k, v)| format!("{}={}", k, v))
304				.collect::<Vec<_>>()
305				.join("; ");
306
307			if let Ok(value) = HeaderValue::from_str(&cookie_str) {
308				self.headers.insert(http::header::COOKIE, value);
309			}
310		}
311	}
312
313	fn update_uri_query(&mut self) {
314		let path = self.uri.path().to_string();
315		if self.query_params.is_empty() {
316			if let Ok(uri) = path.parse() {
317				self.uri = uri;
318			}
319		} else {
320			let query: String = self
321				.query_params
322				.iter()
323				.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
324				.collect::<Vec<_>>()
325				.join("&");
326
327			if let Ok(uri) = format!("{}?{}", path, query).parse() {
328				self.uri = uri;
329			}
330		}
331	}
332}
333
334/// Mock HTTP response for testing.
335///
336/// This represents a response that can be returned from a server function
337/// or used for assertions.
338#[derive(Debug, Clone)]
339pub struct MockHttpResponse {
340	/// HTTP status code.
341	pub status: StatusCode,
342	/// Response headers.
343	pub headers: HeaderMap,
344	/// Response body.
345	pub body: Bytes,
346}
347
348impl Default for MockHttpResponse {
349	fn default() -> Self {
350		Self {
351			status: StatusCode::OK,
352			headers: HeaderMap::new(),
353			body: Bytes::new(),
354		}
355	}
356}
357
358impl MockHttpResponse {
359	/// Create a new mock response with the given status.
360	pub fn new(status: StatusCode) -> Self {
361		Self {
362			status,
363			..Default::default()
364		}
365	}
366
367	/// Create a successful (200 OK) response.
368	pub fn ok() -> Self {
369		Self::new(StatusCode::OK)
370	}
371
372	/// Create a created (201 Created) response.
373	pub fn created() -> Self {
374		Self::new(StatusCode::CREATED)
375	}
376
377	/// Create a no content (204 No Content) response.
378	pub fn no_content() -> Self {
379		Self::new(StatusCode::NO_CONTENT)
380	}
381
382	/// Create a bad request (400) response.
383	pub fn bad_request() -> Self {
384		Self::new(StatusCode::BAD_REQUEST)
385	}
386
387	/// Create an unauthorized (401) response.
388	pub fn unauthorized() -> Self {
389		Self::new(StatusCode::UNAUTHORIZED)
390	}
391
392	/// Create a forbidden (403) response.
393	pub fn forbidden() -> Self {
394		Self::new(StatusCode::FORBIDDEN)
395	}
396
397	/// Create a not found (404) response.
398	pub fn not_found() -> Self {
399		Self::new(StatusCode::NOT_FOUND)
400	}
401
402	/// Create an internal server error (500) response.
403	pub fn internal_error() -> Self {
404		Self::new(StatusCode::INTERNAL_SERVER_ERROR)
405	}
406
407	/// Create a JSON response.
408	pub fn json<T: Serialize>(body: &T) -> Self {
409		let mut response = Self::ok();
410		if let Ok(bytes) = serde_json::to_vec(body) {
411			response.body = Bytes::from(bytes);
412			response.headers.insert(
413				http::header::CONTENT_TYPE,
414				HeaderValue::from_static("application/json"),
415			);
416		}
417		response
418	}
419
420	/// Create a text response.
421	pub fn text(body: impl Into<String>) -> Self {
422		let mut response = Self::ok();
423		response.body = Bytes::from(body.into());
424		response.headers.insert(
425			http::header::CONTENT_TYPE,
426			HeaderValue::from_static("text/plain"),
427		);
428		response
429	}
430
431	/// Set the response body as JSON.
432	pub fn with_json<T: Serialize>(mut self, body: &T) -> Self {
433		if let Ok(bytes) = serde_json::to_vec(body) {
434			self.body = Bytes::from(bytes);
435			self.headers.insert(
436				http::header::CONTENT_TYPE,
437				HeaderValue::from_static("application/json"),
438			);
439		}
440		self
441	}
442
443	/// Set the response body.
444	pub fn with_body(mut self, body: impl Into<Bytes>) -> Self {
445		self.body = body.into();
446		self
447	}
448
449	/// Set the status code.
450	pub fn with_status(mut self, status: StatusCode) -> Self {
451		self.status = status;
452		self
453	}
454
455	/// Add a header.
456	pub fn with_header(mut self, name: &str, value: &str) -> Self {
457		if let (Ok(header_name), Ok(header_value)) = (
458			HeaderName::from_bytes(name.as_bytes()),
459			HeaderValue::from_str(value),
460		) {
461			self.headers.insert(header_name, header_value);
462		}
463		self
464	}
465
466	/// Add a Set-Cookie header.
467	pub fn with_cookie(mut self, name: &str, value: &str, options: Option<CookieOptions>) -> Self {
468		let opts = options.unwrap_or_default();
469		let mut cookie = format!("{}={}", name, value);
470
471		if let Some(max_age) = opts.max_age {
472			cookie.push_str(&format!("; Max-Age={}", max_age));
473		}
474		if let Some(ref path) = opts.path {
475			cookie.push_str(&format!("; Path={}", path));
476		}
477		if let Some(ref domain) = opts.domain {
478			cookie.push_str(&format!("; Domain={}", domain));
479		}
480		if opts.secure {
481			cookie.push_str("; Secure");
482		}
483		if opts.http_only {
484			cookie.push_str("; HttpOnly");
485		}
486		if let Some(ref same_site) = opts.same_site {
487			cookie.push_str(&format!("; SameSite={}", same_site));
488		}
489
490		if let Ok(value) = HeaderValue::from_str(&cookie) {
491			self.headers.append(http::header::SET_COOKIE, value);
492		}
493		self
494	}
495
496	/// Check if the response is successful (2xx).
497	pub fn is_success(&self) -> bool {
498		self.status.is_success()
499	}
500
501	/// Check if the response is a client error (4xx).
502	pub fn is_client_error(&self) -> bool {
503		self.status.is_client_error()
504	}
505
506	/// Check if the response is a server error (5xx).
507	pub fn is_server_error(&self) -> bool {
508		self.status.is_server_error()
509	}
510
511	/// Get a header value.
512	pub fn get_header(&self, name: &str) -> Option<&str> {
513		self.headers.get(name).and_then(|v| v.to_str().ok())
514	}
515
516	/// Parse the body as JSON.
517	pub fn json_body<T: for<'de> Deserialize<'de>>(&self) -> Result<T, serde_json::Error> {
518		serde_json::from_slice(&self.body)
519	}
520
521	/// Get the body as a string.
522	pub fn text_body(&self) -> Result<String, std::string::FromUtf8Error> {
523		String::from_utf8(self.body.to_vec())
524	}
525}
526
527/// Cookie options for Set-Cookie header.
528#[derive(Debug, Clone, Default)]
529pub struct CookieOptions {
530	/// Max-Age in seconds.
531	pub max_age: Option<i64>,
532	/// Cookie path.
533	pub path: Option<String>,
534	/// Cookie domain.
535	pub domain: Option<String>,
536	/// Secure flag.
537	pub secure: bool,
538	/// HttpOnly flag.
539	pub http_only: bool,
540	/// SameSite attribute.
541	pub same_site: Option<String>,
542}
543
544impl CookieOptions {
545	/// Create new default cookie options.
546	pub fn new() -> Self {
547		Self::default()
548	}
549
550	/// Set max age in seconds.
551	pub fn max_age(mut self, seconds: i64) -> Self {
552		self.max_age = Some(seconds);
553		self
554	}
555
556	/// Set the path.
557	pub fn path(mut self, path: impl Into<String>) -> Self {
558		self.path = Some(path.into());
559		self
560	}
561
562	/// Set the domain.
563	pub fn domain(mut self, domain: impl Into<String>) -> Self {
564		self.domain = Some(domain.into());
565		self
566	}
567
568	/// Enable secure flag.
569	pub fn secure(mut self) -> Self {
570		self.secure = true;
571		self
572	}
573
574	/// Enable HTTP-only flag.
575	pub fn http_only(mut self) -> Self {
576		self.http_only = true;
577		self
578	}
579
580	/// Set SameSite to Strict.
581	pub fn same_site_strict(mut self) -> Self {
582		self.same_site = Some("Strict".to_string());
583		self
584	}
585
586	/// Set SameSite to Lax.
587	pub fn same_site_lax(mut self) -> Self {
588		self.same_site = Some("Lax".to_string());
589		self
590	}
591
592	/// Set SameSite to None.
593	pub fn same_site_none(mut self) -> Self {
594		self.same_site = Some("None".to_string());
595		self.secure = true; // None requires Secure
596		self
597	}
598}
599
600#[cfg(test)]
601mod tests {
602	use super::*;
603	use serde::ser::Error as _;
604
605	#[test]
606	fn test_mock_request_get() {
607		let request = MockHttpRequest::get("/api/users");
608		assert_eq!(request.method, Method::GET);
609		assert_eq!(request.path(), "/api/users");
610	}
611
612	#[test]
613	fn test_mock_request_post_json() {
614		#[derive(Serialize)]
615		struct Input {
616			name: String,
617		}
618
619		let request = MockHttpRequest::post("/api/users").with_json(&Input {
620			name: "Alice".to_string(),
621		});
622
623		assert_eq!(request.method, Method::POST);
624		assert_eq!(request.get_header("content-type"), Some("application/json"));
625		assert!(request.text().unwrap().contains("Alice"));
626	}
627
628	#[test]
629	fn test_mock_request_with_headers() {
630		let request = MockHttpRequest::get("/api")
631			.with_header("X-Custom", "value")
632			.with_bearer_token("token123");
633
634		assert_eq!(request.get_header("x-custom"), Some("value"));
635		assert_eq!(request.get_header("authorization"), Some("Bearer token123"));
636	}
637
638	#[test]
639	fn test_mock_request_with_cookies() {
640		let request = MockHttpRequest::get("/api")
641			.with_cookie("session", "abc")
642			.with_cookie("user", "123");
643
644		assert_eq!(request.get_cookie("session"), Some("abc"));
645		assert_eq!(request.get_cookie("user"), Some("123"));
646	}
647
648	#[test]
649	fn test_mock_request_with_query() {
650		let request = MockHttpRequest::get("/api/search")
651			.with_query("q", "test")
652			.with_query("page", "1");
653
654		assert_eq!(request.get_query("q"), Some("test"));
655		assert_eq!(request.get_query("page"), Some("1"));
656	}
657
658	#[test]
659	fn test_mock_response_json() {
660		#[derive(Serialize, Deserialize, PartialEq, Debug)]
661		struct Output {
662			id: i32,
663		}
664
665		let response = MockHttpResponse::json(&Output { id: 1 });
666
667		assert!(response.is_success());
668		assert_eq!(
669			response.get_header("content-type"),
670			Some("application/json")
671		);
672
673		let body: Output = response.json_body().unwrap();
674		assert_eq!(body.id, 1);
675	}
676
677	#[test]
678	fn test_mock_response_with_cookie() {
679		let response = MockHttpResponse::ok().with_cookie(
680			"session",
681			"xyz",
682			Some(
683				CookieOptions::new()
684					.max_age(3600)
685					.path("/")
686					.secure()
687					.http_only(),
688			),
689		);
690
691		let cookie = response.get_header("set-cookie").unwrap();
692		assert!(cookie.contains("session=xyz"));
693		assert!(cookie.contains("Max-Age=3600"));
694		assert!(cookie.contains("Path=/"));
695		assert!(cookie.contains("Secure"));
696		assert!(cookie.contains("HttpOnly"));
697	}
698
699	#[test]
700	fn mock_http_request_builders_preserve_method_body_auth_cookies_and_query() {
701		#[derive(Debug, Deserialize, PartialEq, Serialize)]
702		struct Input {
703			name: String,
704			count: u8,
705		}
706
707		// Arrange
708		let json_input = Input {
709			name: "Ada".into(),
710			count: 3,
711		};
712		let json = MockHttpRequest::put("/api/items?existing=first&existing=last")
713			.with_json(&json_input)
714			.with_headers([("X-Request-ID", "req-7"), ("Accept", "application/json")])
715			.with_cookies([("theme", "light"), ("session", "old")])
716			.with_cookie("session", "new")
717			.with_query_params([("page", "1"), ("filter", "ready")])
718			.with_query("page", "2")
719			.with_bearer_token("token-123");
720		let form = MockHttpRequest::patch("/api/items/7").with_form(&json_input);
721		let text = MockHttpRequest::delete("/api/items/7").with_text("remove");
722		let raw =
723			MockHttpRequest::new(Method::PUT, "/raw").with_body(Bytes::from_static(b"\x00\x01"));
724		let basic_password = std::process::id().to_string();
725		let basic = MockHttpRequest::delete("/admin")
726			.with_basic_auth("alice", &basic_password)
727			.with_content_type("application/custom")
728			.with_accept("text/plain");
729
730		// Act
731		let decoded_json: Input = json.json().unwrap();
732		let decoded_form: Input = form.form().unwrap();
733		let mut query: Vec<_> = json
734			.uri
735			.query()
736			.unwrap()
737			.split('&')
738			.map(|entry| entry.split_once('=').unwrap())
739			.map(|(key, value)| (key.to_string(), value.to_string()))
740			.collect();
741		query.sort_unstable();
742		let mut cookies: Vec<_> = json
743			.get_header("cookie")
744			.expect("Cookie header should contain the configured cookies")
745			.split("; ")
746			.map(|entry| {
747				entry
748					.split_once('=')
749					.expect("Cookie header entries should contain a name and value")
750			})
751			.map(|(name, value)| (name.to_string(), value.to_string()))
752			.collect();
753		cookies.sort_unstable();
754
755		// Assert
756		assert_eq!(json.method, Method::PUT);
757		assert_eq!(json.path(), "/api/items");
758		assert_eq!(json.uri_string().starts_with("/api/items?"), true);
759		assert_eq!(decoded_json, json_input);
760		assert_eq!(json.get_header("content-type"), Some("application/json"));
761		assert_eq!(json.get_header("x-request-id"), Some("req-7"));
762		assert_eq!(json.get_header("accept"), Some("application/json"));
763		assert_eq!(json.get_header("authorization"), Some("Bearer token-123"));
764		assert_eq!(cookies.len(), 2);
765		assert_eq!(
766			cookies,
767			vec![
768				("session".to_string(), "new".to_string()),
769				("theme".to_string(), "light".to_string()),
770			]
771		);
772		assert_eq!(json.get_cookie("theme"), Some("light"));
773		assert_eq!(json.get_cookie("session"), Some("new"));
774		assert_eq!(json.query_params.get("existing"), Some(&"last".to_string()));
775		assert_eq!(json.query_params.get("page"), Some(&"2".to_string()));
776		assert_eq!(json.query_params.get("filter"), Some(&"ready".to_string()));
777		assert_eq!(query.len(), 3);
778		assert_eq!(
779			query,
780			vec![
781				("existing".to_string(), "last".to_string()),
782				("filter".to_string(), "ready".to_string()),
783				("page".to_string(), "2".to_string()),
784			]
785		);
786		assert_eq!(form.method, Method::PATCH);
787		assert_eq!(
788			form.get_header("content-type"),
789			Some("application/x-www-form-urlencoded")
790		);
791		assert_eq!(
792			decoded_form,
793			Input {
794				name: "Ada".into(),
795				count: 3
796			}
797		);
798		assert_eq!(text.method, Method::DELETE);
799		assert_eq!(text.get_header("content-type"), Some("text/plain"));
800		assert_eq!(text.text().unwrap(), "remove");
801		assert_eq!(raw.body, Bytes::from_static(b"\x00\x01"));
802		let encoded_basic_authorization = basic
803			.get_header("authorization")
804			.unwrap()
805			.strip_prefix("Basic ")
806			.unwrap();
807		let decoded_basic_authorization = base64_simd::STANDARD
808			.decode_to_vec(encoded_basic_authorization)
809			.unwrap();
810		assert_eq!(
811			String::from_utf8(decoded_basic_authorization).unwrap(),
812			format!("alice:{}", basic_password)
813		);
814		assert_eq!(basic.get_header("content-type"), Some("application/custom"));
815		assert_eq!(basic.get_header("accept"), Some("text/plain"));
816	}
817
818	#[test]
819	fn mock_http_response_builders_preserve_status_body_headers_and_cookie_policy() {
820		#[derive(Debug, Deserialize, PartialEq, Serialize)]
821		struct Output {
822			id: u8,
823		}
824
825		struct FailingSerialize;
826
827		impl Serialize for FailingSerialize {
828			fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
829			where
830				S: serde::Serializer,
831			{
832				Err(S::Error::custom("cannot serialize"))
833			}
834		}
835
836		// Arrange
837		let statuses = [
838			MockHttpResponse::ok().status,
839			MockHttpResponse::created().status,
840			MockHttpResponse::no_content().status,
841			MockHttpResponse::bad_request().status,
842			MockHttpResponse::unauthorized().status,
843			MockHttpResponse::forbidden().status,
844			MockHttpResponse::not_found().status,
845			MockHttpResponse::internal_error().status,
846		];
847		let response = MockHttpResponse::created()
848			.with_json(&Output { id: 7 })
849			.with_header("X-Trace", "trace-9")
850			.with_cookie(
851				"strict",
852				"one",
853				Some(
854					CookieOptions::new()
855						.max_age(60)
856						.path("/")
857						.http_only()
858						.same_site_strict(),
859				),
860			)
861			.with_cookie(
862				"lax",
863				"two",
864				Some(CookieOptions::new().domain("example.test").same_site_lax()),
865			)
866			.with_cookie("none", "three", Some(CookieOptions::new().same_site_none()));
867		let unchanged = MockHttpResponse::text("keep").with_json(&FailingSerialize);
868		let failed_new = MockHttpResponse::json(&FailingSerialize);
869
870		// Act
871		let decoded: Output = response.json_body().unwrap();
872		let cookies: Vec<_> = response
873			.headers
874			.get_all(http::header::SET_COOKIE)
875			.iter()
876			.map(|value| value.to_str().unwrap().to_string())
877			.collect();
878
879		// Assert
880		assert_eq!(
881			statuses,
882			[
883				StatusCode::OK,
884				StatusCode::CREATED,
885				StatusCode::NO_CONTENT,
886				StatusCode::BAD_REQUEST,
887				StatusCode::UNAUTHORIZED,
888				StatusCode::FORBIDDEN,
889				StatusCode::NOT_FOUND,
890				StatusCode::INTERNAL_SERVER_ERROR,
891			]
892		);
893		assert_eq!(response.status, StatusCode::CREATED);
894		assert_eq!(response.is_success(), true);
895		assert_eq!(response.is_client_error(), false);
896		assert_eq!(response.is_server_error(), false);
897		assert_eq!(decoded, Output { id: 7 });
898		assert_eq!(
899			response.get_header("content-type"),
900			Some("application/json")
901		);
902		assert_eq!(response.get_header("x-trace"), Some("trace-9"));
903		assert_eq!(
904			cookies,
905			vec![
906				"strict=one; Max-Age=60; Path=/; HttpOnly; SameSite=Strict",
907				"lax=two; Domain=example.test; SameSite=Lax",
908				"none=three; Secure; SameSite=None",
909			]
910		);
911		assert_eq!(MockHttpResponse::bad_request().is_client_error(), true);
912		assert_eq!(MockHttpResponse::internal_error().is_server_error(), true);
913		assert_eq!(unchanged.status, StatusCode::OK);
914		assert_eq!(unchanged.text_body().unwrap(), "keep");
915		assert_eq!(unchanged.get_header("content-type"), Some("text/plain"));
916		assert_eq!(failed_new.body, Bytes::new());
917		assert_eq!(failed_new.headers.len(), 0);
918	}
919}