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