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	user: Option<Value>,
194}
195
196impl RequestBuilder {
197	/// Create a new request builder with the given HTTP method, path, and default headers.
198	pub fn new(method: Method, path: &str, default_headers: &HeaderMap) -> Self {
199		Self {
200			method,
201			path: path.to_string(),
202			headers: default_headers.clone(),
203			query_params: HashMap::new(),
204			body: None,
205			format: "json".to_string(),
206			user: None,
207		}
208	}
209	/// Get the HTTP method of this request.
210	pub fn method(&self) -> Method {
211		self.method.clone()
212	}
213	/// Get the path of this request.
214	pub fn path(&self) -> &str {
215		&self.path
216	}
217	/// Set the content format for this request.
218	pub fn with_format(mut self, format: &str) -> Self {
219		self.format = format.to_string();
220		self
221	}
222	// Fixes #865
223	/// Add a custom HTTP header to this request builder.
224	pub fn header(mut self, name: &str, value: &str) -> Result<Self, ClientError> {
225		let header_name: http::header::HeaderName = name
226			.parse()
227			.map_err(|_| ClientError::RequestFailed(format!("Invalid header name: {}", name)))?;
228		self.headers
229			.insert(header_name, HeaderValue::from_str(value)?);
230		Ok(self)
231	}
232	/// Add a query parameter to this request.
233	pub fn query(mut self, key: &str, value: &str) -> Self {
234		self.query_params.insert(key.to_string(), value.to_string());
235		self
236	}
237	/// Add a query parameter (alias for `query`).
238	pub fn query_param(self, key: &str, value: &str) -> Self {
239		self.query(key, value)
240	}
241	/// Set request body as JSON
242	///
243	/// # Examples
244	///
245	/// ```
246	/// use reinhardt_testkit::factory::APIRequestFactory;
247	/// use serde_json::json;
248	///
249	/// let factory = APIRequestFactory::new();
250	/// let data = json!({"name": "test"});
251	/// let request = factory.post("/api/users/").json(&data).unwrap().build();
252	/// ```
253	pub fn json<T: Serialize>(mut self, data: &T) -> Result<Self, ClientError> {
254		let json = serde_json::to_vec(data)?;
255		self.body = Some(Bytes::from(json));
256		self.format = "json".to_string();
257		Ok(self)
258	}
259	/// Set request body as form data
260	///
261	/// # Examples
262	///
263	/// ```
264	/// use reinhardt_testkit::factory::APIRequestFactory;
265	/// use serde_json::json;
266	///
267	/// let factory = APIRequestFactory::new();
268	/// let data = json!({"name": "test", "age": 30});
269	/// let request = factory.post("/api/users/").form(&data).unwrap().build();
270	/// ```
271	pub fn form<T: Serialize>(mut self, data: &T) -> Result<Self, ClientError> {
272		let json_value = serde_json::to_value(data)?;
273		if let Value::Object(map) = json_value {
274			let form_data = map
275				.iter()
276				.map(|(k, v)| {
277					let value_str = match v {
278						Value::String(s) => s.clone(),
279						_ => v.to_string(),
280					};
281					format!(
282						"{}={}",
283						url::form_urlencoded::byte_serialize(k.as_bytes()).collect::<String>(),
284						url::form_urlencoded::byte_serialize(value_str.as_bytes())
285							.collect::<String>()
286					)
287				})
288				.collect::<Vec<_>>()
289				.join("&");
290			self.body = Some(Bytes::from(form_data));
291			self.format = "form".to_string();
292			Ok(self)
293		} else {
294			Err(ClientError::RequestFailed(
295				"Expected object for form data".to_string(),
296			))
297		}
298	}
299	/// Set raw body
300	///
301	/// # Examples
302	///
303	/// ```
304	/// use reinhardt_testkit::factory::APIRequestFactory;
305	///
306	/// let factory = APIRequestFactory::new();
307	/// let request = factory.post("/api/upload/").body("raw data").build().unwrap();
308	/// ```
309	pub fn body(mut self, body: impl Into<Bytes>) -> Self {
310		self.body = Some(body.into());
311		self
312	}
313	/// Force authenticate as user (for testing)
314	///
315	/// # Examples
316	///
317	/// ```
318	/// use reinhardt_testkit::factory::APIRequestFactory;
319	/// use serde_json::json;
320	///
321	/// let factory = APIRequestFactory::new();
322	/// let user = json!({"id": 1, "username": "testuser"});
323	/// let request = factory.get("/api/profile/").force_authenticate(user).build().unwrap();
324	/// ```
325	#[deprecated(
326		since = "0.1.0-rc.16",
327		note = "use `client.auth().session()` or `client.auth().jwt()` instead"
328	)]
329	pub fn force_authenticate(mut self, user: Value) -> Self {
330		self.user = Some(user);
331		self
332	}
333	/// Build the request
334	///
335	/// # Examples
336	///
337	/// ```
338	/// use reinhardt_testkit::factory::APIRequestFactory;
339	///
340	/// let factory = APIRequestFactory::new();
341	/// let request = factory.get("/api/users/").build().unwrap();
342	/// assert_eq!(request.method(), "GET");
343	/// ```
344	pub fn build(self) -> Result<Request<Full<Bytes>>, ClientError> {
345		let mut url = self.path.clone();
346
347		// Add query parameters
348		if !self.query_params.is_empty() {
349			let query_string = self
350				.query_params
351				.iter()
352				.map(|(k, v)| {
353					format!(
354						"{}={}",
355						url::form_urlencoded::byte_serialize(k.as_bytes()).collect::<String>(),
356						url::form_urlencoded::byte_serialize(v.as_bytes()).collect::<String>()
357					)
358				})
359				.collect::<Vec<_>>()
360				.join("&");
361			url = format!("{}?{}", url, query_string);
362		}
363
364		let mut request = Request::builder().method(self.method).uri(url);
365
366		// Add headers
367		for (name, value) in self.headers.iter() {
368			request = request.header(name, value);
369		}
370
371		// Add content type based on format
372		if self.body.is_some() {
373			let content_type = match self.format.as_str() {
374				"json" => "application/json",
375				"form" => "application/x-www-form-urlencoded",
376				_ => "application/octet-stream",
377			};
378			request = request.header("Content-Type", content_type);
379		}
380
381		// Add authentication marker if user is set
382		if self.user.is_some() {
383			request = request.header("X-Test-User", "authenticated");
384		}
385
386		// Build request with body
387		let body = self.body.unwrap_or_default();
388		let req = request.body(Full::new(body))?;
389
390		Ok(req)
391	}
392}
393
394#[cfg(test)]
395mod tests {
396	use super::*;
397	use rstest::rstest;
398	use serde_json::json;
399
400	// ========================================================================
401	// Normal: APIRequestFactory
402	// ========================================================================
403
404	#[rstest]
405	fn test_factory_new() {
406		// Arrange
407		let factory = APIRequestFactory::new();
408
409		// Act
410		let request = factory.get("/api/users/").build().unwrap();
411
412		// Assert
413		assert_eq!(request.method(), Method::GET);
414	}
415
416	#[rstest]
417	fn test_factory_default() {
418		// Arrange
419		let factory_new = APIRequestFactory::new();
420		let factory_default = APIRequestFactory::default();
421
422		// Act
423		let req_new = factory_new.get("/test").build().unwrap();
424		let req_default = factory_default.get("/test").build().unwrap();
425
426		// Assert
427		assert_eq!(req_new.method(), req_default.method());
428		assert_eq!(req_new.uri(), req_default.uri());
429	}
430
431	#[rstest]
432	fn test_factory_with_format() {
433		// Arrange
434		let factory = APIRequestFactory::new().with_format("xml");
435
436		// Act
437		let request = factory.post("/api/data/").body("payload").build().unwrap();
438
439		// Assert
440		assert_eq!(
441			request.headers().get("Content-Type").unwrap(),
442			"application/octet-stream"
443		);
444	}
445
446	#[rstest]
447	fn test_factory_with_header() {
448		// Arrange
449		let factory = APIRequestFactory::new()
450			.with_header("X-Custom", "value123")
451			.unwrap();
452
453		// Act
454		let request = factory.get("/api/items/").build().unwrap();
455
456		// Assert
457		assert_eq!(request.headers().get("x-custom").unwrap(), "value123");
458	}
459
460	#[rstest]
461	fn test_factory_get() {
462		// Arrange
463		let factory = APIRequestFactory::new();
464
465		// Act
466		let request = factory.get("/api/users/").build().unwrap();
467
468		// Assert
469		assert_eq!(request.method(), Method::GET);
470	}
471
472	#[rstest]
473	fn test_factory_post() {
474		// Arrange
475		let factory = APIRequestFactory::new();
476
477		// Act
478		let request = factory.post("/api/users/").build().unwrap();
479
480		// Assert
481		assert_eq!(request.method(), Method::POST);
482	}
483
484	#[rstest]
485	fn test_factory_put() {
486		// Arrange
487		let factory = APIRequestFactory::new();
488
489		// Act
490		let request = factory.put("/api/users/1/").build().unwrap();
491
492		// Assert
493		assert_eq!(request.method(), Method::PUT);
494	}
495
496	#[rstest]
497	fn test_factory_patch() {
498		// Arrange
499		let factory = APIRequestFactory::new();
500
501		// Act
502		let request = factory.patch("/api/users/1/").build().unwrap();
503
504		// Assert
505		assert_eq!(request.method(), Method::PATCH);
506	}
507
508	#[rstest]
509	fn test_factory_delete() {
510		// Arrange
511		let factory = APIRequestFactory::new();
512
513		// Act
514		let request = factory.delete("/api/users/1/").build().unwrap();
515
516		// Assert
517		assert_eq!(request.method(), Method::DELETE);
518	}
519
520	#[rstest]
521	fn test_factory_head() {
522		// Arrange
523		let factory = APIRequestFactory::new();
524
525		// Act
526		let request = factory.head("/api/users/").build().unwrap();
527
528		// Assert
529		assert_eq!(request.method(), Method::HEAD);
530	}
531
532	#[rstest]
533	fn test_factory_options() {
534		// Arrange
535		let factory = APIRequestFactory::new();
536
537		// Act
538		let request = factory.options("/api/users/").build().unwrap();
539
540		// Assert
541		assert_eq!(request.method(), Method::OPTIONS);
542	}
543
544	#[rstest]
545	fn test_factory_request_custom() {
546		// Arrange
547		let factory = APIRequestFactory::new();
548
549		// Act
550		let request = factory
551			.request(Method::TRACE, "/api/trace/")
552			.build()
553			.unwrap();
554
555		// Assert
556		assert_eq!(request.method(), Method::TRACE);
557	}
558
559	// ========================================================================
560	// Normal: RequestBuilder
561	// ========================================================================
562
563	#[rstest]
564	fn test_builder_json() {
565		// Arrange
566		let factory = APIRequestFactory::new();
567		let data = json!({"name": "test"});
568
569		// Act
570		let request = factory
571			.post("/api/users/")
572			.json(&data)
573			.unwrap()
574			.build()
575			.unwrap();
576
577		// Assert
578		assert_eq!(
579			request.headers().get("Content-Type").unwrap(),
580			"application/json"
581		);
582		assert_eq!(request.method(), Method::POST);
583	}
584
585	#[rstest]
586	fn test_builder_form() {
587		// Arrange
588		let factory = APIRequestFactory::new();
589		let data = json!({"name": "test", "age": 30});
590
591		// Act
592		let request = factory
593			.post("/api/users/")
594			.form(&data)
595			.unwrap()
596			.build()
597			.unwrap();
598
599		// Assert
600		assert_eq!(
601			request.headers().get("Content-Type").unwrap(),
602			"application/x-www-form-urlencoded"
603		);
604	}
605
606	#[rstest]
607	fn test_builder_raw_body() {
608		// Arrange
609		let factory = APIRequestFactory::new();
610
611		// Act
612		let request = factory
613			.post("/api/upload/")
614			.body("raw data")
615			.build()
616			.unwrap();
617
618		// Assert
619		assert_eq!(request.method(), Method::POST);
620		assert_eq!(
621			request.headers().get("Content-Type").unwrap(),
622			"application/json"
623		);
624	}
625
626	#[rstest]
627	fn test_builder_query_single() {
628		// Arrange
629		let factory = APIRequestFactory::new();
630
631		// Act
632		let request = factory
633			.get("/api/users/")
634			.query("page", "1")
635			.build()
636			.unwrap();
637
638		// Assert
639		assert_eq!(request.uri().to_string(), "/api/users/?page=1");
640	}
641
642	#[rstest]
643	fn test_builder_query_multiple() {
644		// Arrange
645		let factory = APIRequestFactory::new();
646
647		// Act
648		let request = factory
649			.get("/api/users/")
650			.query("page", "1")
651			.query_param("limit", "10")
652			.build()
653			.unwrap();
654
655		// Assert
656		let uri = request.uri().to_string();
657		assert!(uri.contains("page=1"));
658		assert!(uri.contains("limit=10"));
659		assert!(uri.contains('&'));
660	}
661
662	#[rstest]
663	#[allow(deprecated)]
664	fn test_builder_force_authenticate() {
665		// Arrange
666		let factory = APIRequestFactory::new();
667		let user = json!({"id": 1, "username": "testuser"});
668
669		// Act
670		let request = factory
671			.get("/api/profile/")
672			.force_authenticate(user)
673			.build()
674			.unwrap();
675
676		// Assert
677		assert_eq!(
678			request.headers().get("X-Test-User").unwrap(),
679			"authenticated"
680		);
681	}
682
683	#[rstest]
684	fn test_builder_method_getter() {
685		// Arrange
686		let factory = APIRequestFactory::new();
687
688		// Act
689		let builder = factory.get("/test");
690
691		// Assert
692		assert_eq!(builder.method(), Method::GET);
693	}
694
695	#[rstest]
696	fn test_builder_path_getter() {
697		// Arrange
698		let factory = APIRequestFactory::new();
699
700		// Act
701		let builder = factory.get("/api/items/");
702
703		// Assert
704		assert_eq!(builder.path(), "/api/items/");
705	}
706
707	#[rstest]
708	fn test_builder_with_format() {
709		// Arrange
710		let factory = APIRequestFactory::new();
711
712		// Act
713		let request = factory
714			.post("/api/data/")
715			.with_format("form")
716			.body("key=val")
717			.build()
718			.unwrap();
719
720		// Assert
721		assert_eq!(
722			request.headers().get("Content-Type").unwrap(),
723			"application/x-www-form-urlencoded"
724		);
725	}
726
727	// ========================================================================
728	// Error cases
729	// ========================================================================
730
731	#[rstest]
732	fn test_factory_with_header_invalid_name() {
733		// Arrange / Act
734		let result = APIRequestFactory::new().with_header("invalid header!", "value");
735
736		// Assert
737		assert!(result.is_err());
738	}
739
740	#[rstest]
741	fn test_builder_form_non_object() {
742		// Arrange
743		let factory = APIRequestFactory::new();
744		let data = json!([1, 2, 3]);
745
746		// Act
747		let result = factory.post("/api/users/").form(&data);
748
749		// Assert
750		assert!(result.is_err());
751	}
752
753	#[rstest]
754	fn test_builder_header_invalid_name() {
755		// Arrange
756		let factory = APIRequestFactory::new();
757
758		// Act
759		let result = factory.get("/test").header("bad header!", "value");
760
761		// Assert
762		assert!(result.is_err());
763	}
764
765	// ========================================================================
766	// Edge cases
767	// ========================================================================
768
769	#[rstest]
770	fn test_builder_no_body_no_content_type() {
771		// Arrange
772		let factory = APIRequestFactory::new();
773
774		// Act
775		let request = factory.get("/api/users/").build().unwrap();
776
777		// Assert
778		assert!(request.headers().get("Content-Type").is_none());
779	}
780
781	#[rstest]
782	fn test_builder_json_empty_object() {
783		// Arrange
784		let factory = APIRequestFactory::new();
785		let data = json!({});
786
787		// Act
788		let request = factory
789			.post("/api/data/")
790			.json(&data)
791			.unwrap()
792			.build()
793			.unwrap();
794
795		// Assert
796		assert_eq!(
797			request.headers().get("Content-Type").unwrap(),
798			"application/json"
799		);
800	}
801
802	#[rstest]
803	fn test_builder_query_special_chars() {
804		// Arrange
805		let factory = APIRequestFactory::new();
806
807		// Act
808		let request = factory
809			.get("/api/search/")
810			.query("q", "hello world&foo=bar")
811			.build()
812			.unwrap();
813
814		// Assert
815		let uri = request.uri().to_string();
816		assert!(uri.contains("hello+world"));
817		assert!(!uri.contains("hello world&foo=bar"));
818	}
819
820	#[rstest]
821	fn test_builder_unknown_format() {
822		// Arrange
823		let factory = APIRequestFactory::new().with_format("xml");
824
825		// Act
826		let request = factory.post("/api/data/").body("<xml/>").build().unwrap();
827
828		// Assert
829		assert_eq!(
830			request.headers().get("Content-Type").unwrap(),
831			"application/octet-stream"
832		);
833	}
834}