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}