Skip to main content

reinhardt_testkit/
http.rs

1//! HTTP test utilities for Reinhardt framework
2//!
3//! Provides helper functions for creating and manipulating HTTP requests and responses in tests.
4
5use bytes::Bytes;
6use hyper::header::{HeaderName, HeaderValue};
7use hyper::{HeaderMap, Method, StatusCode, Uri, Version};
8use serde::de::DeserializeOwned;
9use std::str::FromStr;
10
11// Re-export types from reinhardt-apps for convenience
12pub use reinhardt_http::{Error, Request, Response, Result};
13
14/// Create a test HTTP request
15///
16/// This is a convenience function for creating HTTP requests in tests.
17/// Supports both simple request creation and header-based request creation.
18///
19/// # Examples
20///
21/// ## Basic usage
22///
23/// ```
24/// use reinhardt_testkit::http::create_request;
25/// use hyper::Method;
26///
27/// let request = create_request(Method::GET, "/api/users", None, vec![]);
28/// assert_eq!(request.method, Method::GET);
29/// assert_eq!(request.uri.path(), "/api/users");
30/// ```
31///
32/// ## With body
33///
34/// ```
35/// use reinhardt_testkit::http::create_request;
36/// use hyper::Method;
37///
38/// let body = r#"{"name": "Alice"}"#;
39/// let request = create_request(Method::POST, "/api/users", Some(body.to_string()), vec![]);
40/// assert_eq!(request.method, Method::POST);
41/// assert_eq!(request.body().len(), body.len());
42/// ```
43///
44/// ## With headers
45///
46/// ```
47/// use reinhardt_testkit::http::create_request;
48/// use hyper::Method;
49///
50/// let headers = vec![
51///     ("Content-Type", "application/json"),
52///     ("X-API-Key", "secret"),
53/// ];
54/// let request = create_request(Method::GET, "/api/users", None, headers);
55/// assert_eq!(request.method, Method::GET);
56/// assert!(request.headers.contains_key("content-type"));
57/// assert!(request.headers.contains_key("x-api-key"));
58/// ```
59pub fn create_request(
60	method: Method,
61	path: &str,
62	body: Option<String>,
63	headers: Vec<(&str, &str)>,
64) -> Request {
65	let uri = path.parse::<Uri>().expect("Invalid URI");
66	let body_bytes = body.map(Bytes::from).unwrap_or_default();
67
68	let mut header_map = HeaderMap::new();
69	for (key, value) in headers {
70		let header_name: hyper::header::HeaderName = key.parse().expect("Invalid header name");
71		let header_value: hyper::header::HeaderValue = value.parse().expect("Invalid header value");
72		header_map.insert(header_name, header_value);
73	}
74
75	Request::builder()
76		.method(method)
77		.uri(uri)
78		.version(Version::HTTP_11)
79		.headers(header_map)
80		.body(body_bytes)
81		.build()
82		.expect("Failed to build request")
83}
84
85/// Extract and deserialize JSON from a response
86///
87/// Returns the deserialized data or an error if deserialization fails.
88///
89/// # Examples
90///
91/// ```
92/// use reinhardt_testkit::http::{extract_json, create_request};
93/// use reinhardt_http::Response;
94/// use serde::{Deserialize, Serialize};
95///
96/// #[derive(Serialize, Deserialize, PartialEq, Debug)]
97/// struct User {
98///     id: i64,
99///     name: String,
100/// }
101///
102/// let user = User { id: 1, name: "Alice".to_string() };
103/// let json = serde_json::to_string(&user).unwrap();
104/// let response = Response::ok()
105///     .with_header("Content-Type", "application/json")
106///     .with_body(json);
107///
108/// let extracted: User = extract_json(response).unwrap();
109/// assert_eq!(extracted.id, 1);
110/// assert_eq!(extracted.name, "Alice");
111/// ```
112///
113/// # Invalid JSON
114///
115/// ```
116/// use reinhardt_testkit::http::extract_json;
117/// use reinhardt_http::Response;
118/// use serde::Deserialize;
119///
120/// #[derive(Deserialize)]
121/// struct User {
122///     id: i64,
123///     name: String,
124/// }
125///
126/// let response = Response::ok()
127///     .with_header("Content-Type", "application/json")
128///     .with_body("invalid json");
129///
130/// let result: Result<User, _> = extract_json(response);
131/// assert!(result.is_err());
132/// ```
133pub fn extract_json<T: DeserializeOwned>(response: Response) -> Result<T> {
134	serde_json::from_slice(&response.body)
135		.map_err(|e| Error::Serialization(format!("Failed to deserialize response: {}", e)))
136}
137
138// ============================================================================
139// Request Creation Helpers
140// ============================================================================
141
142/// Create a mock HTTP request for testing with secure/insecure mode
143///
144/// This function provides more control over request creation, including
145/// the ability to specify whether the request is secure (HTTPS).
146///
147/// # Arguments
148///
149/// * `method` - HTTP method as string (e.g., "GET", "POST")
150/// * `uri` - Request URI as string
151/// * `secure` - Whether this is an HTTPS request
152///
153/// # Examples
154///
155/// ```
156/// use reinhardt_testkit::http::create_test_request;
157///
158/// let request = create_test_request("GET", "/api/users", false);
159/// assert_eq!(request.method.as_str(), "GET");
160/// assert!(!request.is_secure);
161/// ```
162///
163/// ## Secure request
164///
165/// ```
166/// use reinhardt_testkit::http::create_test_request;
167///
168/// let request = create_test_request("POST", "/api/login", true);
169/// assert!(request.is_secure);
170/// assert!(request.headers.contains_key("x-forwarded-proto"));
171/// ```
172pub fn create_test_request(method: &str, uri: &str, secure: bool) -> Request {
173	let method = Method::from_str(method).unwrap_or(Method::GET);
174	let uri = Uri::from_str(uri).unwrap_or_else(|_| Uri::from_static("/"));
175
176	let mut headers = HeaderMap::new();
177
178	// Add X-Forwarded-Proto header if secure
179	if secure {
180		headers.insert(
181			HeaderName::from_static("x-forwarded-proto"),
182			HeaderValue::from_static("https"),
183		);
184	}
185
186	let mut request = Request::builder()
187		.method(method)
188		.uri(uri)
189		.version(Version::HTTP_11)
190		.headers(headers)
191		.body(Bytes::new())
192		.build()
193		.expect("Failed to build request");
194	request.is_secure = secure;
195	request
196}
197
198/// Create a mock HTTPS request
199///
200/// Convenience wrapper around [`create_test_request`] for creating secure requests.
201///
202/// # Examples
203///
204/// ```
205/// use reinhardt_testkit::http::create_secure_request;
206///
207/// let request = create_secure_request("GET", "/api/users");
208/// assert!(request.is_secure);
209/// assert_eq!(request.method.as_str(), "GET");
210/// ```
211pub fn create_secure_request(method: &str, uri: &str) -> Request {
212	create_test_request(method, uri, true)
213}
214
215/// Create a mock HTTP request (non-secure)
216///
217/// Convenience wrapper around [`create_test_request`] for creating insecure requests.
218///
219/// # Examples
220///
221/// ```
222/// use reinhardt_testkit::http::create_insecure_request;
223///
224/// let request = create_insecure_request("GET", "/api/users");
225/// assert!(!request.is_secure);
226/// assert_eq!(request.method.as_str(), "GET");
227/// ```
228pub fn create_insecure_request(method: &str, uri: &str) -> Request {
229	create_test_request(method, uri, false)
230}
231
232// ============================================================================
233// Response Creation Helpers
234// ============================================================================
235
236/// Create a mock response for testing
237///
238/// Returns a default 200 OK response.
239///
240/// # Examples
241///
242/// ```
243/// use reinhardt_testkit::http::create_test_response;
244/// use hyper::StatusCode;
245///
246/// let response = create_test_response();
247/// assert_eq!(response.status, StatusCode::OK);
248/// ```
249pub fn create_test_response() -> Response {
250	Response::ok()
251}
252
253/// Create a response with custom status code
254///
255/// # Examples
256///
257/// ```
258/// use reinhardt_testkit::http::create_response_with_status;
259/// use hyper::StatusCode;
260///
261/// let response = create_response_with_status(StatusCode::NOT_FOUND);
262/// assert_eq!(response.status, StatusCode::NOT_FOUND);
263/// ```
264pub fn create_response_with_status(status: StatusCode) -> Response {
265	Response::new(status)
266}
267
268/// Create a response with custom headers
269///
270/// # Examples
271///
272/// ```
273/// use reinhardt_testkit::http::create_response_with_headers;
274/// use hyper::{HeaderMap, header::{HeaderName, HeaderValue}};
275///
276/// let mut headers = HeaderMap::new();
277/// headers.insert(
278///     HeaderName::from_static("x-custom-header"),
279///     HeaderValue::from_static("custom-value"),
280/// );
281/// let response = create_response_with_headers(headers);
282/// assert!(response.headers.contains_key("x-custom-header"));
283/// ```
284pub fn create_response_with_headers(headers: HeaderMap) -> Response {
285	let mut response = Response::ok();
286	response.headers = headers;
287	response
288}
289
290// ============================================================================
291// Header Inspection Helpers
292// ============================================================================
293
294/// Check if response has a specific header
295///
296/// # Examples
297///
298/// ```
299/// use reinhardt_testkit::http::{create_test_response, has_header};
300///
301/// let response = create_test_response().with_header("x-api-version", "v1");
302/// assert!(has_header(&response, "x-api-version"));
303/// assert!(!has_header(&response, "x-missing-header"));
304/// ```
305pub fn has_header(response: &Response, header_name: &str) -> bool {
306	response.headers.contains_key(header_name)
307}
308
309/// Get header value from response
310///
311/// Returns `None` if the header is not present or cannot be converted to a string.
312///
313/// # Examples
314///
315/// ```
316/// use reinhardt_testkit::http::{create_test_response, get_header};
317///
318/// let response = create_test_response().with_header("x-api-version", "v1");
319/// assert_eq!(get_header(&response, "x-api-version"), Some("v1"));
320/// assert_eq!(get_header(&response, "x-missing"), None);
321/// ```
322pub fn get_header<'a>(response: &'a Response, header_name: &str) -> Option<&'a str> {
323	response
324		.headers
325		.get(header_name)
326		.and_then(|v| v.to_str().ok())
327}
328
329/// Check if header has specific value
330///
331/// # Examples
332///
333/// ```
334/// use reinhardt_testkit::http::{create_test_response, header_equals};
335///
336/// let response = create_test_response().with_header("content-type", "application/json");
337/// assert!(header_equals(&response, "content-type", "application/json"));
338/// assert!(!header_equals(&response, "content-type", "text/html"));
339/// ```
340pub fn header_equals(response: &Response, header_name: &str, expected_value: &str) -> bool {
341	get_header(response, header_name)
342		.map(|v| v == expected_value)
343		.unwrap_or(false)
344}
345
346/// Check if header contains substring
347///
348/// # Examples
349///
350/// ```
351/// use reinhardt_testkit::http::{create_test_response, header_contains};
352///
353/// let response = create_test_response().with_header("content-type", "application/json; charset=utf-8");
354/// assert!(header_contains(&response, "content-type", "application/json"));
355/// assert!(header_contains(&response, "content-type", "charset"));
356/// assert!(!header_contains(&response, "content-type", "text/html"));
357/// ```
358pub fn header_contains(response: &Response, header_name: &str, substring: &str) -> bool {
359	get_header(response, header_name)
360		.map(|v| v.contains(substring))
361		.unwrap_or(false)
362}
363
364// ============================================================================
365// Response Assertions
366// ============================================================================
367
368/// Assert response status code
369///
370/// Panics if the status code doesn't match the expected value.
371///
372/// # Examples
373///
374/// ```
375/// use reinhardt_testkit::http::{create_test_response, assert_status};
376/// use hyper::StatusCode;
377///
378/// let response = create_test_response();
379/// assert_status(&response, StatusCode::OK); // Passes
380/// ```
381///
382/// ```should_panic
383/// use reinhardt_testkit::http::{create_test_response, assert_status};
384/// use hyper::StatusCode;
385///
386/// let response = create_test_response();
387/// assert_status(&response, StatusCode::NOT_FOUND); // Panics
388/// ```
389pub fn assert_status(response: &Response, expected: StatusCode) {
390	assert_eq!(
391		response.status, expected,
392		"Expected status {}, got {}",
393		expected, response.status
394	);
395}
396
397/// Assert response has header
398///
399/// Panics if the header is not present.
400///
401/// # Examples
402///
403/// ```
404/// use reinhardt_testkit::http::{create_test_response, assert_has_header};
405///
406/// let response = create_test_response().with_header("x-api-version", "v1");
407/// assert_has_header(&response, "x-api-version"); // Passes
408/// ```
409///
410/// ```should_panic
411/// use reinhardt_testkit::http::{create_test_response, assert_has_header};
412///
413/// let response = create_test_response();
414/// assert_has_header(&response, "x-missing-header"); // Panics
415/// ```
416pub fn assert_has_header(response: &Response, header_name: &str) {
417	assert!(
418		has_header(response, header_name),
419		"Expected response to have header '{}'",
420		header_name
421	);
422}
423
424/// Assert response doesn't have header
425///
426/// Panics if the header is present.
427///
428/// # Examples
429///
430/// ```
431/// use reinhardt_testkit::http::{create_test_response, assert_no_header};
432///
433/// let response = create_test_response();
434/// assert_no_header(&response, "x-missing-header"); // Passes
435/// ```
436///
437/// ```should_panic
438/// use reinhardt_testkit::http::{create_test_response, assert_no_header};
439///
440/// let response = create_test_response().with_header("x-api-version", "v1");
441/// assert_no_header(&response, "x-api-version"); // Panics
442/// ```
443pub fn assert_no_header(response: &Response, header_name: &str) {
444	assert!(
445		!has_header(response, header_name),
446		"Expected response to NOT have header '{}'",
447		header_name
448	);
449}
450
451/// Assert header value equals expected
452///
453/// Panics if the header is not present or has a different value.
454///
455/// # Examples
456///
457/// ```
458/// use reinhardt_testkit::http::{create_test_response, assert_header_equals};
459///
460/// let response = create_test_response().with_header("content-type", "application/json");
461/// assert_header_equals(&response, "content-type", "application/json"); // Passes
462/// ```
463///
464/// ```should_panic
465/// use reinhardt_testkit::http::{create_test_response, assert_header_equals};
466///
467/// let response = create_test_response().with_header("content-type", "application/json");
468/// assert_header_equals(&response, "content-type", "text/html"); // Panics
469/// ```
470pub fn assert_header_equals(response: &Response, header_name: &str, expected_value: &str) {
471	let actual = get_header(response, header_name)
472		.unwrap_or_else(|| panic!("Header '{}' not found", header_name));
473	assert_eq!(
474		actual, expected_value,
475		"Expected header '{}' to be '{}', got '{}'",
476		header_name, expected_value, actual
477	);
478}
479
480/// Assert header contains substring
481///
482/// Panics if the header is not present or doesn't contain the expected substring.
483///
484/// # Examples
485///
486/// ```
487/// use reinhardt_testkit::http::{create_test_response, assert_header_contains};
488///
489/// let response = create_test_response().with_header("content-type", "application/json; charset=utf-8");
490/// assert_header_contains(&response, "content-type", "application/json"); // Passes
491/// ```
492///
493/// ```should_panic
494/// use reinhardt_testkit::http::{create_test_response, assert_header_contains};
495///
496/// let response = create_test_response().with_header("content-type", "application/json");
497/// assert_header_contains(&response, "content-type", "text/html"); // Panics
498/// ```
499pub fn assert_header_contains(response: &Response, header_name: &str, substring: &str) {
500	let actual = get_header(response, header_name)
501		.unwrap_or_else(|| panic!("Header '{}' not found", header_name));
502	assert!(
503		actual.contains(substring),
504		"Expected header '{}' to contain '{}', got '{}'",
505		header_name,
506		substring,
507		actual
508	);
509}
510
511#[cfg(test)]
512mod tests {
513	use super::*;
514	use rstest::rstest;
515
516	// ========================================================================
517	// create_request
518	// ========================================================================
519
520	#[rstest]
521	fn test_create_request_get_basic() {
522		// Arrange / Act
523		let request = create_request(Method::GET, "/api/users", None, vec![]);
524
525		// Assert
526		assert_eq!(request.method, Method::GET);
527		assert_eq!(request.uri.path(), "/api/users");
528		assert!(request.body().is_empty());
529	}
530
531	#[rstest]
532	fn test_create_request_post_with_body() {
533		// Arrange
534		let body = r#"{"name": "Alice"}"#;
535
536		// Act
537		let request = create_request(Method::POST, "/api/users", Some(body.to_string()), vec![]);
538
539		// Assert
540		assert_eq!(request.method, Method::POST);
541		assert_eq!(request.body().len(), body.len());
542	}
543
544	#[rstest]
545	fn test_create_request_with_headers() {
546		// Arrange
547		let headers = vec![
548			("Content-Type", "application/json"),
549			("X-API-Key", "secret"),
550		];
551
552		// Act
553		let request = create_request(Method::GET, "/api/users", None, headers);
554
555		// Assert
556		assert!(request.headers.contains_key("content-type"));
557		assert!(request.headers.contains_key("x-api-key"));
558	}
559
560	// ========================================================================
561	// create_test_request
562	// ========================================================================
563
564	#[rstest]
565	fn test_create_test_request_secure() {
566		// Arrange / Act
567		let request = create_test_request("POST", "/api/login", true);
568
569		// Assert
570		assert!(request.is_secure);
571		assert!(request.headers.contains_key("x-forwarded-proto"));
572		assert_eq!(request.method, Method::POST);
573	}
574
575	#[rstest]
576	fn test_create_test_request_insecure() {
577		// Arrange / Act
578		let request = create_test_request("GET", "/api/users", false);
579
580		// Assert
581		assert!(!request.is_secure);
582		assert!(!request.headers.contains_key("x-forwarded-proto"));
583	}
584
585	// ========================================================================
586	// create_secure_request / create_insecure_request
587	// ========================================================================
588
589	#[rstest]
590	fn test_create_secure_request() {
591		// Arrange / Act
592		let request = create_secure_request("GET", "/api/users");
593
594		// Assert
595		assert!(request.is_secure);
596		assert_eq!(request.method, Method::GET);
597	}
598
599	#[rstest]
600	fn test_create_insecure_request() {
601		// Arrange / Act
602		let request = create_insecure_request("GET", "/api/users");
603
604		// Assert
605		assert!(!request.is_secure);
606		assert_eq!(request.method, Method::GET);
607	}
608
609	// ========================================================================
610	// Response creation helpers
611	// ========================================================================
612
613	#[rstest]
614	fn test_create_test_response() {
615		// Arrange / Act
616		let response = create_test_response();
617
618		// Assert
619		assert_eq!(response.status, StatusCode::OK);
620	}
621
622	#[rstest]
623	fn test_create_response_with_status() {
624		// Arrange / Act
625		let response = create_response_with_status(StatusCode::NOT_FOUND);
626
627		// Assert
628		assert_eq!(response.status, StatusCode::NOT_FOUND);
629	}
630
631	#[rstest]
632	fn test_create_response_with_headers() {
633		// Arrange
634		let mut headers = HeaderMap::new();
635		headers.insert(
636			HeaderName::from_static("x-custom-header"),
637			HeaderValue::from_static("custom-value"),
638		);
639
640		// Act
641		let response = create_response_with_headers(headers);
642
643		// Assert
644		assert!(response.headers.contains_key("x-custom-header"));
645	}
646
647	// ========================================================================
648	// extract_json
649	// ========================================================================
650
651	#[rstest]
652	fn test_extract_json_valid() {
653		// Arrange
654		#[derive(serde::Deserialize, PartialEq, Debug)]
655		struct User {
656			id: i64,
657			name: String,
658		}
659		let response = Response::ok()
660			.with_header("Content-Type", "application/json")
661			.with_body(r#"{"id": 1, "name": "Alice"}"#);
662
663		// Act
664		let user: User = extract_json(response).unwrap();
665
666		// Assert
667		assert_eq!(user.id, 1);
668		assert_eq!(user.name, "Alice");
669	}
670
671	#[rstest]
672	fn test_extract_json_invalid() {
673		// Arrange
674		#[derive(serde::Deserialize)]
675		struct User {
676			#[allow(dead_code)] // Field used for deserialization target verification
677			id: i64,
678		}
679		let response = Response::ok().with_body("not json");
680
681		// Act
682		let result: Result<User> = extract_json(response);
683
684		// Assert
685		assert!(result.is_err());
686	}
687
688	// ========================================================================
689	// Header inspection helpers
690	// ========================================================================
691
692	#[rstest]
693	fn test_has_header_present() {
694		// Arrange
695		let response = create_test_response().with_header("x-api-version", "v1");
696
697		// Act / Assert
698		assert!(has_header(&response, "x-api-version"));
699	}
700
701	#[rstest]
702	fn test_has_header_absent() {
703		// Arrange
704		let response = create_test_response();
705
706		// Act / Assert
707		assert!(!has_header(&response, "x-missing"));
708	}
709
710	#[rstest]
711	fn test_get_header_present() {
712		// Arrange
713		let response = create_test_response().with_header("x-api-version", "v1");
714
715		// Act
716		let value = get_header(&response, "x-api-version");
717
718		// Assert
719		assert_eq!(value, Some("v1"));
720	}
721
722	#[rstest]
723	fn test_get_header_absent() {
724		// Arrange
725		let response = create_test_response();
726
727		// Act
728		let value = get_header(&response, "x-missing");
729
730		// Assert
731		assert_eq!(value, None);
732	}
733
734	#[rstest]
735	fn test_header_equals_match() {
736		// Arrange
737		let response = create_test_response().with_header("content-type", "application/json");
738
739		// Act / Assert
740		assert!(header_equals(&response, "content-type", "application/json"));
741	}
742
743	#[rstest]
744	fn test_header_equals_mismatch() {
745		// Arrange
746		let response = create_test_response().with_header("content-type", "application/json");
747
748		// Act / Assert
749		assert!(!header_equals(&response, "content-type", "text/html"));
750	}
751
752	#[rstest]
753	fn test_header_contains_substring() {
754		// Arrange
755		let response =
756			create_test_response().with_header("content-type", "application/json; charset=utf-8");
757
758		// Act / Assert
759		assert!(header_contains(
760			&response,
761			"content-type",
762			"application/json"
763		));
764		assert!(header_contains(&response, "content-type", "charset"));
765	}
766
767	#[rstest]
768	fn test_header_contains_no_match() {
769		// Arrange
770		let response = create_test_response().with_header("content-type", "application/json");
771
772		// Act / Assert
773		assert!(!header_contains(&response, "content-type", "text/html"));
774	}
775
776	// ========================================================================
777	// Response assertions
778	// ========================================================================
779
780	#[rstest]
781	fn test_assert_status_pass() {
782		// Arrange
783		let response = create_test_response();
784
785		// Act / Assert (should not panic)
786		assert_status(&response, StatusCode::OK);
787	}
788
789	#[rstest]
790	#[should_panic(expected = "Expected status")]
791	fn test_assert_status_fail() {
792		// Arrange
793		let response = create_test_response();
794
795		// Act (should panic)
796		assert_status(&response, StatusCode::NOT_FOUND);
797	}
798
799	#[rstest]
800	fn test_assert_has_header_pass() {
801		// Arrange
802		let response = create_test_response().with_header("x-api-version", "v1");
803
804		// Act / Assert (should not panic)
805		assert_has_header(&response, "x-api-version");
806	}
807
808	#[rstest]
809	#[should_panic(expected = "Expected response to have header")]
810	fn test_assert_has_header_fail() {
811		// Arrange
812		let response = create_test_response();
813
814		// Act (should panic)
815		assert_has_header(&response, "x-missing");
816	}
817
818	#[rstest]
819	fn test_assert_no_header_pass() {
820		// Arrange
821		let response = create_test_response();
822
823		// Act / Assert (should not panic)
824		assert_no_header(&response, "x-missing");
825	}
826
827	#[rstest]
828	#[should_panic(expected = "Expected response to NOT have header")]
829	fn test_assert_no_header_fail() {
830		// Arrange
831		let response = create_test_response().with_header("x-api-version", "v1");
832
833		// Act (should panic)
834		assert_no_header(&response, "x-api-version");
835	}
836}