Skip to main content

reinhardt_testkit/
factory.rs

1//! Request factory for creating test requests
2//!
3//! Similar to DRF's APIRequestFactory
4
5use bytes::Bytes;
6use http::{HeaderMap, HeaderValue, Method, Request};
7use http_body_util::Full;
8use serde::Serialize;
9use serde_json::Value;
10use std::collections::HashMap;
11
12use crate::client::ClientError;
13
14/// Factory for creating test requests
15pub struct APIRequestFactory {
16	default_format: String,
17	default_headers: HeaderMap,
18}
19
20impl APIRequestFactory {
21	/// Create a new request factory
22	///
23	/// # Examples
24	///
25	/// ```
26	/// use reinhardt_testkit::factory::APIRequestFactory;
27	///
28	/// let factory = APIRequestFactory::new();
29	/// let request = factory.get("/api/users/").build();
30	/// ```
31	pub fn new() -> Self {
32		Self {
33			default_format: "json".to_string(),
34			default_headers: HeaderMap::new(),
35		}
36	}
37	/// Set the default content format (e.g., `"json"`, `"xml"`).
38	pub fn with_format(mut self, format: impl Into<String>) -> Self {
39		self.default_format = format.into();
40		self
41	}
42	/// Add a default header to all requests created by this factory.
43	pub fn with_header(
44		mut self,
45		name: impl AsRef<str>,
46		value: impl AsRef<str>,
47	) -> Result<Self, ClientError> {
48		let header_name: http::header::HeaderName = name.as_ref().parse().map_err(|_| {
49			ClientError::RequestFailed(format!("Invalid header name: {}", name.as_ref()))
50		})?;
51		self.default_headers
52			.insert(header_name, HeaderValue::from_str(value.as_ref())?);
53		Ok(self)
54	}
55	/// Create a GET request
56	///
57	/// # Examples
58	///
59	/// ```
60	/// use reinhardt_testkit::factory::APIRequestFactory;
61	///
62	/// let factory = APIRequestFactory::new();
63	/// let request = factory.get("/api/users/").build().unwrap();
64	/// assert_eq!(request.method(), "GET");
65	/// ```
66	pub fn get(&self, path: &str) -> RequestBuilder {
67		RequestBuilder::new(Method::GET, path, &self.default_headers)
68	}
69	/// Create a POST request
70	///
71	/// # Examples
72	///
73	/// ```
74	/// use reinhardt_testkit::factory::APIRequestFactory;
75	/// use serde_json::json;
76	///
77	/// let factory = APIRequestFactory::new();
78	/// let data = json!({"name": "test"});
79	/// let request = factory.post("/api/users/").json(&data).unwrap().build().unwrap();
80	/// assert_eq!(request.method(), "POST");
81	/// ```
82	pub fn post(&self, path: &str) -> RequestBuilder {
83		RequestBuilder::new(Method::POST, path, &self.default_headers)
84			.with_format(&self.default_format)
85	}
86	/// Create a PUT request
87	///
88	/// # Examples
89	///
90	/// ```
91	/// use reinhardt_testkit::factory::APIRequestFactory;
92	/// use serde_json::json;
93	///
94	/// let factory = APIRequestFactory::new();
95	/// let data = json!({"name": "updated"});
96	/// let request = factory.put("/api/users/1/").json(&data).unwrap().build().unwrap();
97	/// assert_eq!(request.method(), "PUT");
98	/// ```
99	pub fn put(&self, path: &str) -> RequestBuilder {
100		RequestBuilder::new(Method::PUT, path, &self.default_headers)
101			.with_format(&self.default_format)
102	}
103	/// Create a PATCH request
104	///
105	/// # Examples
106	///
107	/// ```
108	/// use reinhardt_testkit::factory::APIRequestFactory;
109	/// use serde_json::json;
110	///
111	/// let factory = APIRequestFactory::new();
112	/// let data = json!({"name": "partial_update"});
113	/// let request = factory.patch("/api/users/1/").json(&data).unwrap().build().unwrap();
114	/// assert_eq!(request.method(), "PATCH");
115	/// ```
116	pub fn patch(&self, path: &str) -> RequestBuilder {
117		RequestBuilder::new(Method::PATCH, path, &self.default_headers)
118			.with_format(&self.default_format)
119	}
120	/// Create a DELETE request
121	///
122	/// # Examples
123	///
124	/// ```
125	/// use reinhardt_testkit::factory::APIRequestFactory;
126	///
127	/// let factory = APIRequestFactory::new();
128	/// let request = factory.delete("/api/users/1/").build().unwrap();
129	/// assert_eq!(request.method(), "DELETE");
130	/// ```
131	pub fn delete(&self, path: &str) -> RequestBuilder {
132		RequestBuilder::new(Method::DELETE, path, &self.default_headers)
133	}
134	/// Create a HEAD request
135	///
136	/// # Examples
137	///
138	/// ```
139	/// use reinhardt_testkit::factory::APIRequestFactory;
140	///
141	/// let factory = APIRequestFactory::new();
142	/// let request = factory.head("/api/users/").build().unwrap();
143	/// assert_eq!(request.method(), "HEAD");
144	/// ```
145	pub fn head(&self, path: &str) -> RequestBuilder {
146		RequestBuilder::new(Method::HEAD, path, &self.default_headers)
147	}
148	/// Create an OPTIONS request
149	///
150	/// # Examples
151	///
152	/// ```
153	/// use reinhardt_testkit::factory::APIRequestFactory;
154	///
155	/// let factory = APIRequestFactory::new();
156	/// let request = factory.options("/api/users/").build().unwrap();
157	/// assert_eq!(request.method(), "OPTIONS");
158	/// ```
159	pub fn options(&self, path: &str) -> RequestBuilder {
160		RequestBuilder::new(Method::OPTIONS, path, &self.default_headers)
161	}
162	/// Create a generic request with custom method
163	///
164	/// # Examples
165	///
166	/// ```
167	/// use reinhardt_testkit::factory::APIRequestFactory;
168	/// use http::Method;
169	///
170	/// let factory = APIRequestFactory::new();
171	/// let request = factory.request(Method::TRACE, "/api/trace/").build().unwrap();
172	/// assert_eq!(request.method(), "TRACE");
173	/// ```
174	pub fn request(&self, method: Method, path: &str) -> RequestBuilder {
175		RequestBuilder::new(method, path, &self.default_headers)
176	}
177}
178
179impl Default for APIRequestFactory {
180	fn default() -> Self {
181		Self::new()
182	}
183}
184
185/// Builder for constructing test requests
186pub struct RequestBuilder {
187	method: Method,
188	path: String,
189	headers: HeaderMap,
190	query_params: HashMap<String, String>,
191	body: Option<Bytes>,
192	format: String,
193}
194
195impl RequestBuilder {
196	/// Create a new request builder with the given HTTP method, path, and default headers.
197	pub fn new(method: Method, path: &str, default_headers: &HeaderMap) -> Self {
198		Self {
199			method,
200			path: path.to_string(),
201			headers: default_headers.clone(),
202			query_params: HashMap::new(),
203			body: None,
204			format: "json".to_string(),
205		}
206	}
207	/// Get the HTTP method of this request.
208	pub fn method(&self) -> Method {
209		self.method.clone()
210	}
211	/// Get the path of this request.
212	pub fn path(&self) -> &str {
213		&self.path
214	}
215	/// Set the content format for this request.
216	pub fn with_format(mut self, format: &str) -> Self {
217		self.format = format.to_string();
218		self
219	}
220	// Fixes #865
221	/// Add a custom HTTP header to this request builder.
222	pub fn header(mut self, name: &str, value: &str) -> Result<Self, ClientError> {
223		let header_name: http::header::HeaderName = name
224			.parse()
225			.map_err(|_| ClientError::RequestFailed(format!("Invalid header name: {}", name)))?;
226		self.headers
227			.insert(header_name, HeaderValue::from_str(value)?);
228		Ok(self)
229	}
230	/// Add a query parameter to this request.
231	pub fn query(mut self, key: &str, value: &str) -> Self {
232		self.query_params.insert(key.to_string(), value.to_string());
233		self
234	}
235	/// Add a query parameter (alias for `query`).
236	pub fn query_param(self, key: &str, value: &str) -> Self {
237		self.query(key, value)
238	}
239	/// Set request body as JSON
240	///
241	/// # Examples
242	///
243	/// ```
244	/// use reinhardt_testkit::factory::APIRequestFactory;
245	/// use serde_json::json;
246	///
247	/// let factory = APIRequestFactory::new();
248	/// let data = json!({"name": "test"});
249	/// let request = factory.post("/api/users/").json(&data).unwrap().build();
250	/// ```
251	pub fn json<T: Serialize>(mut self, data: &T) -> Result<Self, ClientError> {
252		let json = serde_json::to_vec(data)?;
253		self.body = Some(Bytes::from(json));
254		self.format = "json".to_string();
255		Ok(self)
256	}
257	/// Set request body as form data
258	///
259	/// # Examples
260	///
261	/// ```
262	/// use reinhardt_testkit::factory::APIRequestFactory;
263	/// use serde_json::json;
264	///
265	/// let factory = APIRequestFactory::new();
266	/// let data = json!({"name": "test", "age": 30});
267	/// let request = factory.post("/api/users/").form(&data).unwrap().build();
268	/// ```
269	pub fn form<T: Serialize>(mut self, data: &T) -> Result<Self, ClientError> {
270		let json_value = serde_json::to_value(data)?;
271		if let Value::Object(map) = json_value {
272			let form_data = map
273				.iter()
274				.map(|(k, v)| {
275					let value_str = match v {
276						Value::String(s) => s.clone(),
277						_ => v.to_string(),
278					};
279					format!(
280						"{}={}",
281						url::form_urlencoded::byte_serialize(k.as_bytes()).collect::<String>(),
282						url::form_urlencoded::byte_serialize(value_str.as_bytes())
283							.collect::<String>()
284					)
285				})
286				.collect::<Vec<_>>()
287				.join("&");
288			self.body = Some(Bytes::from(form_data));
289			self.format = "form".to_string();
290			Ok(self)
291		} else {
292			Err(ClientError::RequestFailed(
293				"Expected object for form data".to_string(),
294			))
295		}
296	}
297	/// Set raw body
298	///
299	/// # Examples
300	///
301	/// ```
302	/// use reinhardt_testkit::factory::APIRequestFactory;
303	///
304	/// let factory = APIRequestFactory::new();
305	/// let request = factory.post("/api/upload/").body("raw data").build().unwrap();
306	/// ```
307	pub fn body(mut self, body: impl Into<Bytes>) -> Self {
308		self.body = Some(body.into());
309		self
310	}
311	/// Build the request
312	///
313	/// # Examples
314	///
315	/// ```
316	/// use reinhardt_testkit::factory::APIRequestFactory;
317	///
318	/// let factory = APIRequestFactory::new();
319	/// let request = factory.get("/api/users/").build().unwrap();
320	/// assert_eq!(request.method(), "GET");
321	/// ```
322	pub fn build(self) -> Result<Request<Full<Bytes>>, ClientError> {
323		let mut url = self.path.clone();
324
325		// Add query parameters
326		if !self.query_params.is_empty() {
327			let query_string = self
328				.query_params
329				.iter()
330				.map(|(k, v)| {
331					format!(
332						"{}={}",
333						url::form_urlencoded::byte_serialize(k.as_bytes()).collect::<String>(),
334						url::form_urlencoded::byte_serialize(v.as_bytes()).collect::<String>()
335					)
336				})
337				.collect::<Vec<_>>()
338				.join("&");
339			url = format!("{}?{}", url, query_string);
340		}
341
342		let mut request = Request::builder().method(self.method).uri(url);
343
344		// Add headers
345		for (name, value) in self.headers.iter() {
346			request = request.header(name, value);
347		}
348
349		// Add content type based on format
350		if self.body.is_some() {
351			let content_type = match self.format.as_str() {
352				"json" => "application/json",
353				"form" => "application/x-www-form-urlencoded",
354				_ => "application/octet-stream",
355			};
356			request = request.header("Content-Type", content_type);
357		}
358
359		// Build request with body
360		let body = self.body.unwrap_or_default();
361		let req = request.body(Full::new(body))?;
362
363		Ok(req)
364	}
365}
366
367#[cfg(test)]
368mod tests {
369	use super::*;
370	use rstest::rstest;
371	use serde_json::json;
372
373	// ========================================================================
374	// Normal: APIRequestFactory
375	// ========================================================================
376
377	#[rstest]
378	fn test_factory_new() {
379		// Arrange
380		let factory = APIRequestFactory::new();
381
382		// Act
383		let request = factory.get("/api/users/").build().unwrap();
384
385		// Assert
386		assert_eq!(request.method(), Method::GET);
387	}
388
389	#[rstest]
390	fn test_factory_default() {
391		// Arrange
392		let factory_new = APIRequestFactory::new();
393		let factory_default = APIRequestFactory::default();
394
395		// Act
396		let req_new = factory_new.get("/test").build().unwrap();
397		let req_default = factory_default.get("/test").build().unwrap();
398
399		// Assert
400		assert_eq!(req_new.method(), req_default.method());
401		assert_eq!(req_new.uri(), req_default.uri());
402	}
403
404	#[rstest]
405	fn test_factory_with_format() {
406		// Arrange
407		let factory = APIRequestFactory::new().with_format("xml");
408
409		// Act
410		let request = factory.post("/api/data/").body("payload").build().unwrap();
411
412		// Assert
413		assert_eq!(
414			request.headers().get("Content-Type").unwrap(),
415			"application/octet-stream"
416		);
417	}
418
419	#[rstest]
420	fn test_factory_with_header() {
421		// Arrange
422		let factory = APIRequestFactory::new()
423			.with_header("X-Custom", "value123")
424			.unwrap();
425
426		// Act
427		let request = factory.get("/api/items/").build().unwrap();
428
429		// Assert
430		assert_eq!(request.headers().get("x-custom").unwrap(), "value123");
431	}
432
433	#[rstest]
434	fn test_factory_get() {
435		// Arrange
436		let factory = APIRequestFactory::new();
437
438		// Act
439		let request = factory.get("/api/users/").build().unwrap();
440
441		// Assert
442		assert_eq!(request.method(), Method::GET);
443	}
444
445	#[rstest]
446	fn test_factory_post() {
447		// Arrange
448		let factory = APIRequestFactory::new();
449
450		// Act
451		let request = factory.post("/api/users/").build().unwrap();
452
453		// Assert
454		assert_eq!(request.method(), Method::POST);
455	}
456
457	#[rstest]
458	fn test_factory_put() {
459		// Arrange
460		let factory = APIRequestFactory::new();
461
462		// Act
463		let request = factory.put("/api/users/1/").build().unwrap();
464
465		// Assert
466		assert_eq!(request.method(), Method::PUT);
467	}
468
469	#[rstest]
470	fn test_factory_patch() {
471		// Arrange
472		let factory = APIRequestFactory::new();
473
474		// Act
475		let request = factory.patch("/api/users/1/").build().unwrap();
476
477		// Assert
478		assert_eq!(request.method(), Method::PATCH);
479	}
480
481	#[rstest]
482	fn test_factory_delete() {
483		// Arrange
484		let factory = APIRequestFactory::new();
485
486		// Act
487		let request = factory.delete("/api/users/1/").build().unwrap();
488
489		// Assert
490		assert_eq!(request.method(), Method::DELETE);
491	}
492
493	#[rstest]
494	fn test_factory_head() {
495		// Arrange
496		let factory = APIRequestFactory::new();
497
498		// Act
499		let request = factory.head("/api/users/").build().unwrap();
500
501		// Assert
502		assert_eq!(request.method(), Method::HEAD);
503	}
504
505	#[rstest]
506	fn test_factory_options() {
507		// Arrange
508		let factory = APIRequestFactory::new();
509
510		// Act
511		let request = factory.options("/api/users/").build().unwrap();
512
513		// Assert
514		assert_eq!(request.method(), Method::OPTIONS);
515	}
516
517	#[rstest]
518	fn test_factory_request_custom() {
519		// Arrange
520		let factory = APIRequestFactory::new();
521
522		// Act
523		let request = factory
524			.request(Method::TRACE, "/api/trace/")
525			.build()
526			.unwrap();
527
528		// Assert
529		assert_eq!(request.method(), Method::TRACE);
530	}
531
532	// ========================================================================
533	// Normal: RequestBuilder
534	// ========================================================================
535
536	#[rstest]
537	fn test_builder_json() {
538		// Arrange
539		let factory = APIRequestFactory::new();
540		let data = json!({"name": "test"});
541
542		// Act
543		let request = factory
544			.post("/api/users/")
545			.json(&data)
546			.unwrap()
547			.build()
548			.unwrap();
549
550		// Assert
551		assert_eq!(
552			request.headers().get("Content-Type").unwrap(),
553			"application/json"
554		);
555		assert_eq!(request.method(), Method::POST);
556	}
557
558	#[rstest]
559	fn test_builder_form() {
560		// Arrange
561		let factory = APIRequestFactory::new();
562		let data = json!({"name": "test", "age": 30});
563
564		// Act
565		let request = factory
566			.post("/api/users/")
567			.form(&data)
568			.unwrap()
569			.build()
570			.unwrap();
571
572		// Assert
573		assert_eq!(
574			request.headers().get("Content-Type").unwrap(),
575			"application/x-www-form-urlencoded"
576		);
577	}
578
579	#[rstest]
580	fn test_builder_raw_body() {
581		// Arrange
582		let factory = APIRequestFactory::new();
583
584		// Act
585		let request = factory
586			.post("/api/upload/")
587			.body("raw data")
588			.build()
589			.unwrap();
590
591		// Assert
592		assert_eq!(request.method(), Method::POST);
593		assert_eq!(
594			request.headers().get("Content-Type").unwrap(),
595			"application/json"
596		);
597	}
598
599	#[rstest]
600	fn test_builder_query_single() {
601		// Arrange
602		let factory = APIRequestFactory::new();
603
604		// Act
605		let request = factory
606			.get("/api/users/")
607			.query("page", "1")
608			.build()
609			.unwrap();
610
611		// Assert
612		assert_eq!(request.uri().to_string(), "/api/users/?page=1");
613	}
614
615	#[rstest]
616	fn test_builder_query_multiple() {
617		// Arrange
618		let factory = APIRequestFactory::new();
619
620		// Act
621		let request = factory
622			.get("/api/users/")
623			.query("page", "1")
624			.query_param("limit", "10")
625			.build()
626			.unwrap();
627
628		// Assert
629		let uri = request.uri().to_string();
630		assert!(uri.contains("page=1"));
631		assert!(uri.contains("limit=10"));
632		assert!(uri.contains('&'));
633	}
634
635	#[rstest]
636	fn test_builder_method_getter() {
637		// Arrange
638		let factory = APIRequestFactory::new();
639
640		// Act
641		let builder = factory.get("/test");
642
643		// Assert
644		assert_eq!(builder.method(), Method::GET);
645	}
646
647	#[rstest]
648	fn test_builder_path_getter() {
649		// Arrange
650		let factory = APIRequestFactory::new();
651
652		// Act
653		let builder = factory.get("/api/items/");
654
655		// Assert
656		assert_eq!(builder.path(), "/api/items/");
657	}
658
659	#[rstest]
660	fn test_builder_with_format() {
661		// Arrange
662		let factory = APIRequestFactory::new();
663
664		// Act
665		let request = factory
666			.post("/api/data/")
667			.with_format("form")
668			.body("key=val")
669			.build()
670			.unwrap();
671
672		// Assert
673		assert_eq!(
674			request.headers().get("Content-Type").unwrap(),
675			"application/x-www-form-urlencoded"
676		);
677	}
678
679	// ========================================================================
680	// Error cases
681	// ========================================================================
682
683	#[rstest]
684	fn test_factory_with_header_invalid_name() {
685		// Arrange / Act
686		let result = APIRequestFactory::new().with_header("invalid header!", "value");
687
688		// Assert
689		assert!(result.is_err());
690	}
691
692	#[rstest]
693	fn test_builder_form_non_object() {
694		// Arrange
695		let factory = APIRequestFactory::new();
696		let data = json!([1, 2, 3]);
697
698		// Act
699		let result = factory.post("/api/users/").form(&data);
700
701		// Assert
702		assert!(result.is_err());
703	}
704
705	#[rstest]
706	fn test_builder_header_invalid_name() {
707		// Arrange
708		let factory = APIRequestFactory::new();
709
710		// Act
711		let result = factory.get("/test").header("bad header!", "value");
712
713		// Assert
714		assert!(result.is_err());
715	}
716
717	// ========================================================================
718	// Edge cases
719	// ========================================================================
720
721	#[rstest]
722	fn test_builder_no_body_no_content_type() {
723		// Arrange
724		let factory = APIRequestFactory::new();
725
726		// Act
727		let request = factory.get("/api/users/").build().unwrap();
728
729		// Assert
730		assert!(request.headers().get("Content-Type").is_none());
731	}
732
733	#[rstest]
734	fn test_builder_json_empty_object() {
735		// Arrange
736		let factory = APIRequestFactory::new();
737		let data = json!({});
738
739		// Act
740		let request = factory
741			.post("/api/data/")
742			.json(&data)
743			.unwrap()
744			.build()
745			.unwrap();
746
747		// Assert
748		assert_eq!(
749			request.headers().get("Content-Type").unwrap(),
750			"application/json"
751		);
752	}
753
754	#[rstest]
755	fn test_builder_query_special_chars() {
756		// Arrange
757		let factory = APIRequestFactory::new();
758
759		// Act
760		let request = factory
761			.get("/api/search/")
762			.query("q", "hello world&foo=bar")
763			.build()
764			.unwrap();
765
766		// Assert
767		let uri = request.uri().to_string();
768		assert!(uri.contains("hello+world"));
769		assert!(!uri.contains("hello world&foo=bar"));
770	}
771
772	#[rstest]
773	fn test_builder_unknown_format() {
774		// Arrange
775		let factory = APIRequestFactory::new().with_format("xml");
776
777		// Act
778		let request = factory.post("/api/data/").body("<xml/>").build().unwrap();
779
780		// Assert
781		assert_eq!(
782			request.headers().get("Content-Type").unwrap(),
783			"application/octet-stream"
784		);
785	}
786}