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
604	#[test]
605	fn test_mock_request_get() {
606		let request = MockHttpRequest::get("/api/users");
607		assert_eq!(request.method, Method::GET);
608		assert_eq!(request.path(), "/api/users");
609	}
610
611	#[test]
612	fn test_mock_request_post_json() {
613		#[derive(Serialize)]
614		struct Input {
615			name: String,
616		}
617
618		let request = MockHttpRequest::post("/api/users").with_json(&Input {
619			name: "Alice".to_string(),
620		});
621
622		assert_eq!(request.method, Method::POST);
623		assert_eq!(request.get_header("content-type"), Some("application/json"));
624		assert!(request.text().unwrap().contains("Alice"));
625	}
626
627	#[test]
628	fn test_mock_request_with_headers() {
629		let request = MockHttpRequest::get("/api")
630			.with_header("X-Custom", "value")
631			.with_bearer_token("token123");
632
633		assert_eq!(request.get_header("x-custom"), Some("value"));
634		assert_eq!(request.get_header("authorization"), Some("Bearer token123"));
635	}
636
637	#[test]
638	fn test_mock_request_with_cookies() {
639		let request = MockHttpRequest::get("/api")
640			.with_cookie("session", "abc")
641			.with_cookie("user", "123");
642
643		assert_eq!(request.get_cookie("session"), Some("abc"));
644		assert_eq!(request.get_cookie("user"), Some("123"));
645	}
646
647	#[test]
648	fn test_mock_request_with_query() {
649		let request = MockHttpRequest::get("/api/search")
650			.with_query("q", "test")
651			.with_query("page", "1");
652
653		assert_eq!(request.get_query("q"), Some("test"));
654		assert_eq!(request.get_query("page"), Some("1"));
655	}
656
657	#[test]
658	fn test_mock_response_json() {
659		#[derive(Serialize, Deserialize, PartialEq, Debug)]
660		struct Output {
661			id: i32,
662		}
663
664		let response = MockHttpResponse::json(&Output { id: 1 });
665
666		assert!(response.is_success());
667		assert_eq!(
668			response.get_header("content-type"),
669			Some("application/json")
670		);
671
672		let body: Output = response.json_body().unwrap();
673		assert_eq!(body.id, 1);
674	}
675
676	#[test]
677	fn test_mock_response_with_cookie() {
678		let response = MockHttpResponse::ok().with_cookie(
679			"session",
680			"xyz",
681			Some(
682				CookieOptions::new()
683					.max_age(3600)
684					.path("/")
685					.secure()
686					.http_only(),
687			),
688		);
689
690		let cookie = response.get_header("set-cookie").unwrap();
691		assert!(cookie.contains("session=xyz"));
692		assert!(cookie.contains("Max-Age=3600"));
693		assert!(cookie.contains("Path=/"));
694		assert!(cookie.contains("Secure"));
695		assert!(cookie.contains("HttpOnly"));
696	}
697}