Skip to main content

reinhardt_testkit/
views.rs

1//! View test utilities for Reinhardt framework
2//!
3//! Provides test models, request builders, and test views for view testing.
4
5use bytes::Bytes;
6use hyper::{HeaderMap, Method, Uri, Version};
7use reinhardt_http::{Error, Request, Response, Result};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11// ============================================================================
12// Test Models
13// ============================================================================
14
15/// Test model for view tests
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
17pub struct TestModel {
18	/// Primary key identifier.
19	pub id: Option<i64>,
20	/// Display name of the test model.
21	pub name: String,
22	/// URL-safe slug derived from the name.
23	pub slug: String,
24	/// ISO 8601 timestamp of creation.
25	pub created_at: String,
26}
27
28crate::impl_test_model!(TestModel, i64, "test_models");
29
30/// Test model for API view tests
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
32pub struct ApiTestModel {
33	/// Primary key identifier.
34	pub id: Option<i64>,
35	/// Title of the API test model entry.
36	pub title: String,
37	/// Body content of the API test model entry.
38	pub content: String,
39}
40
41crate::impl_test_model!(ApiTestModel, i64, "api_test_models");
42
43// ============================================================================
44// Request Creation Functions
45// ============================================================================
46
47/// Create a test request with the given parameters
48pub fn create_request(
49	method: Method,
50	path: &str,
51	query_params: Option<HashMap<String, String>>,
52	headers: Option<HeaderMap>,
53	body: Option<Bytes>,
54) -> Request {
55	// Fixes #880: URL-encode query parameter keys and values to prevent injection
56	let uri_str = if let Some(ref params) = query_params {
57		let query = params
58			.iter()
59			.map(|(k, v)| format!("{}={}", urlencoding::encode(k), urlencoding::encode(v)))
60			.collect::<Vec<_>>()
61			.join("&");
62		format!("{}?{}", path, query)
63	} else {
64		path.to_string()
65	};
66
67	let uri = uri_str.parse::<Uri>().unwrap();
68	Request::builder()
69		.method(method)
70		.uri(uri)
71		.version(Version::HTTP_11)
72		.headers(headers.unwrap_or_default())
73		.body(body.unwrap_or_default())
74		.build()
75		.expect("Failed to build request")
76}
77
78/// Create a test request with path parameters
79pub fn create_request_with_path_params(
80	method: Method,
81	path: &str,
82	path_params: HashMap<String, String>,
83	query_params: Option<HashMap<String, String>>,
84	headers: Option<HeaderMap>,
85	body: Option<Bytes>,
86) -> Request {
87	let mut request = create_request(method, path, query_params, headers, body);
88	request.path_params = path_params;
89	request
90}
91
92/// Create a test request with headers
93pub fn create_request_with_headers(
94	method: Method,
95	path: &str,
96	headers: HashMap<String, String>,
97	body: Option<Bytes>,
98) -> Request {
99	let mut header_map = HeaderMap::new();
100	for (key, value) in headers {
101		if let (Ok(header_name), Ok(header_value)) = (
102			hyper::header::HeaderName::from_bytes(key.as_bytes()),
103			hyper::header::HeaderValue::from_str(&value),
104		) {
105			header_map.insert(header_name, header_value);
106		}
107	}
108
109	create_request(method, path, None, Some(header_map), body)
110}
111
112/// Create a test request with JSON body
113pub fn create_json_request(method: Method, path: &str, json_data: &serde_json::Value) -> Request {
114	let body = Bytes::from(serde_json::to_vec(json_data).unwrap());
115	let mut headers = HeaderMap::new();
116	headers.insert(
117		hyper::header::CONTENT_TYPE,
118		hyper::header::HeaderValue::from_static("application/json"),
119	);
120
121	create_request(method, path, None, Some(headers), Some(body))
122}
123
124// ============================================================================
125// Test Data Generation
126// ============================================================================
127
128/// Create test objects for list views
129pub fn create_test_objects() -> Vec<TestModel> {
130	vec![
131		TestModel {
132			id: Some(1),
133			name: "First Object".to_string(),
134			slug: "first-object".to_string(),
135			created_at: "2023-01-01T00:00:00Z".to_string(),
136		},
137		TestModel {
138			id: Some(2),
139			name: "Second Object".to_string(),
140			slug: "second-object".to_string(),
141			created_at: "2023-01-02T00:00:00Z".to_string(),
142		},
143		TestModel {
144			id: Some(3),
145			name: "Third Object".to_string(),
146			slug: "third-object".to_string(),
147			created_at: "2023-01-03T00:00:00Z".to_string(),
148		},
149	]
150}
151
152/// Create test objects for API views
153pub fn create_api_test_objects() -> Vec<ApiTestModel> {
154	vec![
155		ApiTestModel {
156			id: Some(1),
157			title: "First Post".to_string(),
158			content: "This is the first post content".to_string(),
159		},
160		ApiTestModel {
161			id: Some(2),
162			title: "Second Post".to_string(),
163			content: "This is the second post content".to_string(),
164		},
165		ApiTestModel {
166			id: Some(3),
167			title: "Third Post".to_string(),
168			content: "This is the third post content".to_string(),
169		},
170	]
171}
172
173/// Create a large set of test objects for pagination testing
174pub fn create_large_test_objects(count: usize) -> Vec<TestModel> {
175	(0..count)
176		.map(|i| TestModel {
177			id: Some(i as i64),
178			name: format!("Object {}", i),
179			slug: format!("object-{}", i),
180			created_at: format!("2023-01-{:02}T00:00:00Z", (i % 30) + 1),
181		})
182		.collect()
183}
184
185// ============================================================================
186// Test Views
187// ============================================================================
188
189/// Create a simple view for testing basic functionality
190pub struct SimpleTestView {
191	/// The response body content.
192	pub content: String,
193	/// HTTP methods that this view accepts.
194	pub allowed_methods: Vec<Method>,
195}
196
197impl SimpleTestView {
198	/// Create a new `SimpleTestView` with the given content, accepting only GET.
199	pub fn new(content: &str) -> Self {
200		Self {
201			content: content.to_string(),
202			allowed_methods: vec![Method::GET],
203		}
204	}
205
206	/// Set the allowed HTTP methods for this view.
207	pub fn with_methods(mut self, methods: Vec<Method>) -> Self {
208		self.allowed_methods = methods;
209		self
210	}
211}
212
213#[async_trait::async_trait]
214impl reinhardt_views::View for SimpleTestView {
215	async fn dispatch(&self, request: Request) -> Result<Response> {
216		if !self.allowed_methods.contains(&request.method) {
217			return Err(Error::Validation(format!(
218				"Method {} not allowed",
219				request.method
220			)));
221		}
222
223		Ok(Response::ok().with_body(self.content.clone().into_bytes()))
224	}
225}
226
227/// Create a view that always returns an error for testing error handling
228pub struct ErrorTestView {
229	/// The error message to return.
230	pub error_message: String,
231	/// The kind of error to return.
232	pub error_kind: ErrorKind,
233}
234
235/// Kind of error that an `ErrorTestView` will produce.
236pub enum ErrorKind {
237	/// HTTP 404 Not Found error.
238	NotFound,
239	/// Validation error (e.g., invalid input).
240	Validation,
241	/// Internal server error.
242	Internal,
243	/// Authentication required error.
244	Authentication,
245	/// Authorization denied error.
246	Authorization,
247}
248
249impl ErrorTestView {
250	/// Create a new `ErrorTestView` with the given message and error kind.
251	pub fn new(error_message: String, error_kind: ErrorKind) -> Self {
252		Self {
253			error_message,
254			error_kind,
255		}
256	}
257
258	/// Create an `ErrorTestView` that returns a 404 Not Found error.
259	pub fn not_found(message: impl Into<String>) -> Self {
260		Self::new(message.into(), ErrorKind::NotFound)
261	}
262
263	/// Create an `ErrorTestView` that returns a validation error.
264	pub fn validation(message: impl Into<String>) -> Self {
265		Self::new(message.into(), ErrorKind::Validation)
266	}
267}
268
269#[async_trait::async_trait]
270impl reinhardt_views::View for ErrorTestView {
271	async fn dispatch(&self, _request: Request) -> Result<Response> {
272		match self.error_kind {
273			ErrorKind::NotFound => Err(Error::NotFound(self.error_message.clone())),
274			ErrorKind::Validation => Err(Error::Validation(self.error_message.clone())),
275			ErrorKind::Internal => Err(Error::Internal(self.error_message.clone())),
276			ErrorKind::Authentication => Err(Error::Authentication(self.error_message.clone())),
277			ErrorKind::Authorization => Err(Error::Authorization(self.error_message.clone())),
278		}
279	}
280}
281
282#[cfg(test)]
283mod tests {
284	use super::*;
285	use rstest::rstest;
286
287	// ========================================================================
288	// create_request tests
289	// ========================================================================
290
291	#[rstest]
292	fn test_create_request_basic_get() {
293		// Arrange
294		let method = Method::GET;
295		let path = "/api/items/";
296
297		// Act
298		let request = create_request(method.clone(), path, None, None, None);
299
300		// Assert
301		assert_eq!(request.method, Method::GET);
302		assert_eq!(request.uri.path(), "/api/items/");
303		assert!(request.uri.query().is_none());
304	}
305
306	#[rstest]
307	fn test_create_request_with_query_params() {
308		// Arrange
309		let method = Method::GET;
310		let path = "/api/items/";
311		let mut params = HashMap::new();
312		params.insert("page".to_string(), "2".to_string());
313		params.insert("limit".to_string(), "10".to_string());
314
315		// Act
316		let request = create_request(method, path, Some(params), None, None);
317
318		// Assert
319		let query = request.uri.query().expect("query string should be present");
320		assert!(query.contains("page=2"));
321		assert!(query.contains("limit=10"));
322	}
323
324	#[rstest]
325	fn test_create_request_with_body() {
326		// Arrange
327		let method = Method::POST;
328		let path = "/api/items/";
329		let body = Bytes::from(b"hello body".to_vec());
330
331		// Act
332		let request = create_request(method, path, None, None, Some(body.clone()));
333
334		// Assert
335		assert_eq!(request.method, Method::POST);
336		assert_eq!(request.body(), &body);
337	}
338
339	#[rstest]
340	fn test_create_request_with_headers_param() {
341		// Arrange
342		let method = Method::GET;
343		let path = "/api/items/";
344		let mut headers = HeaderMap::new();
345		headers.insert(
346			hyper::header::ACCEPT,
347			hyper::header::HeaderValue::from_static("application/json"),
348		);
349
350		// Act
351		let request = create_request(method, path, None, Some(headers), None);
352
353		// Assert
354		assert_eq!(
355			request.headers.get(hyper::header::ACCEPT).unwrap(),
356			"application/json"
357		);
358	}
359
360	#[rstest]
361	fn test_create_request_with_path_params() {
362		// Arrange
363		let method = Method::GET;
364		let path = "/api/items/1/";
365		let mut path_params = HashMap::new();
366		path_params.insert("id".to_string(), "1".to_string());
367
368		// Act
369		let request =
370			create_request_with_path_params(method, path, path_params.clone(), None, None, None);
371
372		// Assert
373		assert_eq!(request.path_params.get("id").unwrap(), "1");
374		assert_eq!(request.path_params.len(), 1);
375	}
376
377	#[rstest]
378	fn test_create_request_with_headers_fn() {
379		// Arrange
380		let method = Method::POST;
381		let path = "/api/items/";
382		let mut headers = HashMap::new();
383		headers.insert("x-custom-header".to_string(), "custom-value".to_string());
384		headers.insert("authorization".to_string(), "Bearer token123".to_string());
385
386		// Act
387		let request = create_request_with_headers(method, path, headers, None);
388
389		// Assert
390		assert_eq!(
391			request.headers.get("x-custom-header").unwrap(),
392			"custom-value"
393		);
394		assert_eq!(
395			request.headers.get("authorization").unwrap(),
396			"Bearer token123"
397		);
398	}
399
400	#[rstest]
401	fn test_create_json_request() {
402		// Arrange
403		let method = Method::POST;
404		let path = "/api/items/";
405		let json_data = serde_json::json!({"name": "test", "value": 42});
406
407		// Act
408		let request = create_json_request(method, path, &json_data);
409
410		// Assert
411		assert_eq!(request.method, Method::POST);
412		assert_eq!(
413			request.headers.get(hyper::header::CONTENT_TYPE).unwrap(),
414			"application/json"
415		);
416		let body_bytes = request.body();
417		let parsed: serde_json::Value = serde_json::from_slice(body_bytes).unwrap();
418		assert_eq!(parsed, json_data);
419	}
420
421	// ========================================================================
422	// Test data generation tests
423	// ========================================================================
424
425	#[rstest]
426	fn test_create_test_objects_count() {
427		// Arrange & Act
428		let objects = create_test_objects();
429
430		// Assert
431		assert_eq!(objects.len(), 3);
432	}
433
434	#[rstest]
435	fn test_create_test_objects_fields() {
436		// Arrange & Act
437		let objects = create_test_objects();
438
439		// Assert
440		for (i, obj) in objects.iter().enumerate() {
441			assert_eq!(obj.id, Some((i + 1) as i64));
442			assert!(
443				!obj.name.is_empty(),
444				"name should not be empty for object {}",
445				i
446			);
447			assert!(
448				!obj.slug.is_empty(),
449				"slug should not be empty for object {}",
450				i
451			);
452			assert!(
453				!obj.created_at.is_empty(),
454				"created_at should not be empty for object {}",
455				i
456			);
457		}
458	}
459
460	#[rstest]
461	fn test_create_api_test_objects_count() {
462		// Arrange & Act
463		let objects = create_api_test_objects();
464
465		// Assert
466		assert_eq!(objects.len(), 3);
467	}
468
469	#[rstest]
470	fn test_create_large_test_objects_100() {
471		// Arrange
472		let count = 100;
473
474		// Act
475		let objects = create_large_test_objects(count);
476
477		// Assert
478		assert_eq!(objects.len(), 100);
479		for (i, obj) in objects.iter().enumerate() {
480			assert_eq!(obj.id, Some(i as i64));
481			assert_eq!(obj.name, format!("Object {}", i));
482			assert_eq!(obj.slug, format!("object-{}", i));
483		}
484	}
485
486	// ========================================================================
487	// SimpleTestView tests
488	// ========================================================================
489
490	#[rstest]
491	#[tokio::test]
492	async fn test_simple_test_view_new_dispatch_get() {
493		// Arrange
494		let view = SimpleTestView::new("Hello, World!");
495		let request = create_request(Method::GET, "/test/", None, None, None);
496
497		// Act
498		let response = reinhardt_views::View::dispatch(&view, request).await;
499
500		// Assert
501		assert!(response.is_ok());
502		let resp = response.unwrap();
503		assert_eq!(resp.body.as_ref(), b"Hello, World!");
504	}
505
506	#[rstest]
507	fn test_simple_test_view_with_methods() {
508		// Arrange & Act
509		let view = SimpleTestView::new("content").with_methods(vec![
510			Method::GET,
511			Method::POST,
512			Method::PUT,
513		]);
514
515		// Assert
516		assert_eq!(view.allowed_methods.len(), 3);
517		assert!(view.allowed_methods.contains(&Method::GET));
518		assert!(view.allowed_methods.contains(&Method::POST));
519		assert!(view.allowed_methods.contains(&Method::PUT));
520	}
521
522	#[rstest]
523	#[tokio::test]
524	async fn test_simple_test_view_method_not_allowed() {
525		// Arrange
526		let view = SimpleTestView::new("content");
527		let request = create_request(Method::POST, "/test/", None, None, None);
528
529		// Act
530		let result = reinhardt_views::View::dispatch(&view, request).await;
531
532		// Assert
533		assert!(result.is_err());
534		let err = result.unwrap_err();
535		let err_msg = err.to_string();
536		assert!(
537			err_msg.contains("Method POST not allowed"),
538			"Expected method not allowed error, got: {}",
539			err_msg
540		);
541	}
542
543	// ========================================================================
544	// ErrorTestView tests
545	// ========================================================================
546
547	#[rstest]
548	#[tokio::test]
549	async fn test_error_test_view_not_found() {
550		// Arrange
551		let view = ErrorTestView::not_found("Resource not found");
552		let request = create_request(Method::GET, "/missing/", None, None, None);
553
554		// Act
555		let result = reinhardt_views::View::dispatch(&view, request).await;
556
557		// Assert
558		assert!(result.is_err());
559		let err = result.unwrap_err();
560		assert!(err.to_string().contains("Resource not found"));
561	}
562
563	#[rstest]
564	#[tokio::test]
565	async fn test_error_test_view_validation() {
566		// Arrange
567		let view = ErrorTestView::validation("Invalid input data");
568		let request = create_request(Method::POST, "/validate/", None, None, None);
569
570		// Act
571		let result = reinhardt_views::View::dispatch(&view, request).await;
572
573		// Assert
574		assert!(result.is_err());
575		let err = result.unwrap_err();
576		assert!(err.to_string().contains("Invalid input data"));
577	}
578
579	#[rstest]
580	#[tokio::test]
581	async fn test_error_test_view_internal() {
582		// Arrange
583		let view = ErrorTestView::new("Server failure".to_string(), ErrorKind::Internal);
584		let request = create_request(Method::GET, "/error/", None, None, None);
585
586		// Act
587		let result = reinhardt_views::View::dispatch(&view, request).await;
588
589		// Assert
590		assert!(result.is_err());
591		let err = result.unwrap_err();
592		assert!(err.to_string().contains("Server failure"));
593	}
594
595	#[rstest]
596	#[tokio::test]
597	async fn test_error_test_view_authentication() {
598		// Arrange
599		let view = ErrorTestView::new("Not authenticated".to_string(), ErrorKind::Authentication);
600		let request = create_request(Method::GET, "/protected/", None, None, None);
601
602		// Act
603		let result = reinhardt_views::View::dispatch(&view, request).await;
604
605		// Assert
606		assert!(result.is_err());
607		let err = result.unwrap_err();
608		assert!(err.to_string().contains("Not authenticated"));
609	}
610
611	#[rstest]
612	#[tokio::test]
613	async fn test_error_test_view_authorization() {
614		// Arrange
615		let view = ErrorTestView::new("Forbidden".to_string(), ErrorKind::Authorization);
616		let request = create_request(Method::GET, "/admin/", None, None, None);
617
618		// Act
619		let result = reinhardt_views::View::dispatch(&view, request).await;
620
621		// Assert
622		assert!(result.is_err());
623		let err = result.unwrap_err();
624		assert!(err.to_string().contains("Forbidden"));
625	}
626
627	// ========================================================================
628	// Edge case tests
629	// ========================================================================
630
631	#[rstest]
632	fn test_create_request_empty_query_params() {
633		// Arrange
634		let params: HashMap<String, String> = HashMap::new();
635
636		// Act
637		let request = create_request(Method::GET, "/api/items/", Some(params), None, None);
638
639		// Assert
640		// Empty params still produces a "?" but no key=value pairs
641		let uri_str = request.uri.to_string();
642		assert!(
643			uri_str == "/api/items/?" || uri_str == "/api/items/",
644			"URI should be path with empty or no query: {}",
645			uri_str
646		);
647	}
648
649	#[rstest]
650	fn test_create_request_query_special_chars() {
651		// Arrange
652		let mut params = HashMap::new();
653		params.insert("search".to_string(), "hello world&foo=bar".to_string());
654
655		// Act
656		let request = create_request(Method::GET, "/api/search/", Some(params), None, None);
657
658		// Assert
659		let query = request.uri.query().expect("query string should be present");
660		// URL-encoded: space becomes %20, & becomes %26, = becomes %3D
661		assert!(
662			query.contains("hello%20world%26foo%3Dbar"),
663			"Special characters should be URL-encoded, got: {}",
664			query
665		);
666	}
667
668	#[rstest]
669	fn test_create_large_test_objects_zero() {
670		// Arrange & Act
671		let objects = create_large_test_objects(0);
672
673		// Assert
674		assert!(objects.is_empty());
675	}
676
677	#[rstest]
678	fn test_test_model_serialization() {
679		// Arrange
680		let model = TestModel {
681			id: Some(42),
682			name: "Test Item".to_string(),
683			slug: "test-item".to_string(),
684			created_at: "2023-06-15T12:00:00Z".to_string(),
685		};
686
687		// Act
688		let json = serde_json::to_string(&model).unwrap();
689		let deserialized: TestModel = serde_json::from_str(&json).unwrap();
690
691		// Assert
692		assert_eq!(model, deserialized);
693	}
694}