Skip to main content

reinhardt_testkit/
response.rs

1//! Test response wrapper with assertion helpers
2
3use bytes::Bytes;
4use http::{HeaderMap, Response, StatusCode, Version};
5use http_body_util::{BodyExt, Full};
6use serde::de::DeserializeOwned;
7use serde_json::Value;
8
9/// Test response wrapper
10pub struct TestResponse {
11	status: StatusCode,
12	headers: HeaderMap,
13	body: Bytes,
14	version: Version,
15}
16
17impl TestResponse {
18	/// Create a new test response (async version for collecting body)
19	///
20	/// # Examples
21	///
22	/// ```
23	/// use reinhardt_testkit::response::TestResponse;
24	/// use http::{Response, StatusCode};
25	/// use http_body_util::Full;
26	/// use bytes::Bytes;
27	///
28	/// # tokio_test::block_on(async {
29	/// let response = Response::builder()
30	///     .status(StatusCode::OK)
31	///     .body(Full::new(Bytes::from("Hello World")))
32	///     .unwrap();
33	/// let test_response = TestResponse::new(response).await;
34	/// assert_eq!(test_response.status(), StatusCode::OK);
35	/// # });
36	/// ```
37	pub async fn new(response: Response<Full<Bytes>>) -> Self {
38		let (parts, body) = response.into_parts();
39
40		// Collect the body bytes
41		let body_bytes = body
42			.collect()
43			.await
44			.map(|collected| collected.to_bytes())
45			.unwrap_or_else(|_| Bytes::new());
46
47		Self {
48			status: parts.status,
49			headers: parts.headers,
50			body: body_bytes,
51			version: parts.version,
52		}
53	}
54
55	/// Create a test response with status, headers, and body (defaults to HTTP/1.1)
56	pub fn with_body(status: StatusCode, headers: HeaderMap, body: Bytes) -> Self {
57		Self {
58			status,
59			headers,
60			body,
61			version: Version::HTTP_11,
62		}
63	}
64
65	/// Create a test response with status, headers, body, and HTTP version
66	pub fn with_body_and_version(
67		status: StatusCode,
68		headers: HeaderMap,
69		body: Bytes,
70		version: Version,
71	) -> Self {
72		Self {
73			status,
74			headers,
75			body,
76			version,
77		}
78	}
79	/// Get response status
80	pub fn status(&self) -> StatusCode {
81		self.status
82	}
83
84	/// Get response status code as u16
85	pub fn status_code(&self) -> u16 {
86		self.status.as_u16()
87	}
88
89	/// Get HTTP version of the response
90	///
91	/// # Examples
92	///
93	/// ```
94	/// use reinhardt_testkit::response::TestResponse;
95	/// use http::{StatusCode, HeaderMap, Version};
96	/// use bytes::Bytes;
97	///
98	/// let response = TestResponse::with_body_and_version(
99	///     StatusCode::OK,
100	///     HeaderMap::new(),
101	///     Bytes::new(),
102	///     Version::HTTP_2,
103	/// );
104	/// assert_eq!(response.version(), Version::HTTP_2);
105	/// ```
106	pub fn version(&self) -> Version {
107		self.version
108	}
109	/// Get response headers
110	pub fn headers(&self) -> &HeaderMap {
111		&self.headers
112	}
113	/// Get response body as bytes
114	pub fn body(&self) -> &Bytes {
115		&self.body
116	}
117	/// Get response body as string
118	pub fn text(&self) -> String {
119		String::from_utf8_lossy(&self.body).to_string()
120	}
121	/// Parse response body as JSON
122	pub fn json<T: DeserializeOwned>(&self) -> Result<T, serde_json::Error> {
123		serde_json::from_slice(&self.body)
124	}
125	/// Parse response body as generic JSON value
126	pub fn json_value(&self) -> Result<Value, serde_json::Error> {
127		serde_json::from_slice(&self.body)
128	}
129	/// Check if response is successful (2xx)
130	pub fn is_success(&self) -> bool {
131		self.status.is_success()
132	}
133	/// Check if response is client error (4xx)
134	pub fn is_client_error(&self) -> bool {
135		self.status.is_client_error()
136	}
137	/// Check if response is server error (5xx)
138	pub fn is_server_error(&self) -> bool {
139		self.status.is_server_error()
140	}
141	/// Get content type header
142	pub fn content_type(&self) -> Option<&str> {
143		self.headers
144			.get("content-type")
145			.and_then(|v| v.to_str().ok())
146	}
147	/// Get header value
148	pub fn header(&self, name: &str) -> Option<&str> {
149		self.headers.get(name).and_then(|v| v.to_str().ok())
150	}
151}
152
153/// Extension trait for Response assertions
154pub trait ResponseExt {
155	/// Assert status code
156	fn assert_status(&self, expected: StatusCode) -> &Self;
157
158	/// Assert 2xx success
159	fn assert_success(&self) -> &Self;
160
161	/// Assert 4xx client error
162	fn assert_client_error(&self) -> &Self;
163
164	/// Assert 5xx server error
165	fn assert_server_error(&self) -> &Self;
166
167	/// Assert specific status codes
168	fn assert_ok(&self) -> &Self;
169	/// Assert that the response status is 201 Created.
170	fn assert_created(&self) -> &Self;
171	/// Assert that the response status is 204 No Content.
172	fn assert_no_content(&self) -> &Self;
173	/// Assert that the response status is 400 Bad Request.
174	fn assert_bad_request(&self) -> &Self;
175	/// Assert that the response status is 401 Unauthorized.
176	fn assert_unauthorized(&self) -> &Self;
177	/// Assert that the response status is 403 Forbidden.
178	fn assert_forbidden(&self) -> &Self;
179	/// Assert that the response status is 404 Not Found.
180	fn assert_not_found(&self) -> &Self;
181}
182
183impl ResponseExt for TestResponse {
184	fn assert_status(&self, expected: StatusCode) -> &Self {
185		assert_eq!(
186			self.status,
187			expected,
188			"Expected status {}, got {}. Body: {}",
189			expected,
190			self.status,
191			self.text()
192		);
193		self
194	}
195
196	fn assert_success(&self) -> &Self {
197		assert!(
198			self.is_success(),
199			"Expected success status (2xx), got {}. Body: {}",
200			self.status,
201			self.text()
202		);
203		self
204	}
205
206	fn assert_client_error(&self) -> &Self {
207		assert!(
208			self.is_client_error(),
209			"Expected client error status (4xx), got {}. Body: {}",
210			self.status,
211			self.text()
212		);
213		self
214	}
215
216	fn assert_server_error(&self) -> &Self {
217		assert!(
218			self.is_server_error(),
219			"Expected server error status (5xx), got {}. Body: {}",
220			self.status,
221			self.text()
222		);
223		self
224	}
225
226	fn assert_ok(&self) -> &Self {
227		self.assert_status(StatusCode::OK)
228	}
229
230	fn assert_created(&self) -> &Self {
231		self.assert_status(StatusCode::CREATED)
232	}
233
234	fn assert_no_content(&self) -> &Self {
235		self.assert_status(StatusCode::NO_CONTENT)
236	}
237
238	fn assert_bad_request(&self) -> &Self {
239		self.assert_status(StatusCode::BAD_REQUEST)
240	}
241
242	fn assert_unauthorized(&self) -> &Self {
243		self.assert_status(StatusCode::UNAUTHORIZED)
244	}
245
246	fn assert_forbidden(&self) -> &Self {
247		self.assert_status(StatusCode::FORBIDDEN)
248	}
249
250	fn assert_not_found(&self) -> &Self {
251		self.assert_status(StatusCode::NOT_FOUND)
252	}
253}
254
255#[cfg(test)]
256mod tests {
257	use super::*;
258	use rstest::rstest;
259
260	// ========================================================================
261	// Helper: build a TestResponse from parts
262	// ========================================================================
263
264	fn make_response(status: u16, body: &[u8]) -> TestResponse {
265		TestResponse::with_body(
266			StatusCode::from_u16(status).unwrap(),
267			HeaderMap::new(),
268			Bytes::from(body.to_vec()),
269		)
270	}
271
272	// ========================================================================
273	// Construction
274	// ========================================================================
275
276	#[rstest]
277	fn test_with_body() {
278		// Arrange
279		let body = Bytes::from("hello");
280
281		// Act
282		let resp = TestResponse::with_body(StatusCode::OK, HeaderMap::new(), body.clone());
283
284		// Assert
285		assert_eq!(resp.status(), StatusCode::OK);
286		assert_eq!(resp.body(), &body);
287		assert_eq!(resp.version(), Version::HTTP_11);
288	}
289
290	#[rstest]
291	fn test_with_body_and_version() {
292		// Arrange / Act
293		let resp = TestResponse::with_body_and_version(
294			StatusCode::CREATED,
295			HeaderMap::new(),
296			Bytes::from("data"),
297			Version::HTTP_2,
298		);
299
300		// Assert
301		assert_eq!(resp.status(), StatusCode::CREATED);
302		assert_eq!(resp.version(), Version::HTTP_2);
303	}
304
305	#[rstest]
306	#[tokio::test]
307	async fn test_new_async() {
308		// Arrange
309		let response = Response::builder()
310			.status(StatusCode::OK)
311			.body(Full::new(Bytes::from("Hello World")))
312			.unwrap();
313
314		// Act
315		let test_resp = TestResponse::new(response).await;
316
317		// Assert
318		assert_eq!(test_resp.status(), StatusCode::OK);
319		assert_eq!(test_resp.text(), "Hello World");
320	}
321
322	// ========================================================================
323	// Getters
324	// ========================================================================
325
326	#[rstest]
327	fn test_status() {
328		// Arrange
329		let resp = make_response(404, b"");
330
331		// Act / Assert
332		assert_eq!(resp.status(), StatusCode::NOT_FOUND);
333	}
334
335	#[rstest]
336	fn test_status_code() {
337		// Arrange
338		let resp = make_response(201, b"");
339
340		// Act / Assert
341		assert_eq!(resp.status_code(), 201);
342	}
343
344	#[rstest]
345	fn test_version() {
346		// Arrange
347		let resp = TestResponse::with_body(StatusCode::OK, HeaderMap::new(), Bytes::new());
348
349		// Act / Assert
350		assert_eq!(resp.version(), Version::HTTP_11);
351	}
352
353	#[rstest]
354	fn test_headers() {
355		// Arrange
356		let mut headers = HeaderMap::new();
357		headers.insert("x-custom", "value".parse().unwrap());
358		let resp = TestResponse::with_body(StatusCode::OK, headers, Bytes::new());
359
360		// Act / Assert
361		assert!(resp.headers().contains_key("x-custom"));
362	}
363
364	#[rstest]
365	fn test_body() {
366		// Arrange
367		let resp = make_response(200, b"body-content");
368
369		// Act / Assert
370		assert_eq!(resp.body().as_ref(), b"body-content");
371	}
372
373	#[rstest]
374	fn test_text() {
375		// Arrange
376		let resp = make_response(200, b"hello text");
377
378		// Act / Assert
379		assert_eq!(resp.text(), "hello text");
380	}
381
382	#[rstest]
383	fn test_text_non_utf8_lossy() {
384		// Arrange
385		let resp = TestResponse::with_body(
386			StatusCode::OK,
387			HeaderMap::new(),
388			Bytes::from(vec![0xFF, 0xFE, 0x68, 0x69]),
389		);
390
391		// Act
392		let text = resp.text();
393
394		// Assert - lossy conversion replaces invalid bytes with replacement character
395		assert!(text.contains("hi"));
396		assert!(text.contains('\u{FFFD}'));
397	}
398
399	// ========================================================================
400	// JSON parsing
401	// ========================================================================
402
403	#[rstest]
404	fn test_json_valid() {
405		// Arrange
406		#[derive(serde::Deserialize, PartialEq, Debug)]
407		struct Item {
408			id: i32,
409		}
410		let resp = make_response(200, br#"{"id": 42}"#);
411
412		// Act
413		let item: Item = resp.json().unwrap();
414
415		// Assert
416		assert_eq!(item.id, 42);
417	}
418
419	#[rstest]
420	fn test_json_invalid() {
421		// Arrange
422		#[derive(serde::Deserialize)]
423		struct Item {
424			#[allow(dead_code)] // Field used for deserialization target verification
425			id: i32,
426		}
427		let resp = make_response(200, b"not json");
428
429		// Act
430		let result: Result<Item, _> = resp.json();
431
432		// Assert
433		assert!(result.is_err());
434	}
435
436	#[rstest]
437	fn test_json_value_valid() {
438		// Arrange
439		let resp = make_response(200, br#"{"key": "value"}"#);
440
441		// Act
442		let val = resp.json_value().unwrap();
443
444		// Assert
445		assert_eq!(val["key"], "value");
446	}
447
448	#[rstest]
449	fn test_json_value_invalid() {
450		// Arrange
451		let resp = make_response(200, b"broken");
452
453		// Act
454		let result = resp.json_value();
455
456		// Assert
457		assert!(result.is_err());
458	}
459
460	// ========================================================================
461	// Status category checks
462	// ========================================================================
463
464	#[rstest]
465	fn test_is_success_200() {
466		// Arrange / Act / Assert
467		assert!(make_response(200, b"").is_success());
468	}
469
470	#[rstest]
471	fn test_is_success_299() {
472		// Arrange / Act / Assert
473		assert!(make_response(299, b"").is_success());
474	}
475
476	#[rstest]
477	fn test_is_success_boundary_199() {
478		// Arrange / Act / Assert
479		assert!(!make_response(199, b"").is_success());
480	}
481
482	#[rstest]
483	fn test_is_success_boundary_300() {
484		// Arrange / Act / Assert
485		assert!(!make_response(300, b"").is_success());
486	}
487
488	#[rstest]
489	fn test_is_client_error_400() {
490		// Arrange / Act / Assert
491		assert!(make_response(400, b"").is_client_error());
492	}
493
494	#[rstest]
495	fn test_is_client_error_boundary_399() {
496		// Arrange / Act / Assert
497		assert!(!make_response(399, b"").is_client_error());
498	}
499
500	#[rstest]
501	fn test_is_client_error_boundary_499() {
502		// Arrange / Act / Assert
503		assert!(make_response(499, b"").is_client_error());
504	}
505
506	#[rstest]
507	fn test_is_server_error_500() {
508		// Arrange / Act / Assert
509		assert!(make_response(500, b"").is_server_error());
510	}
511
512	#[rstest]
513	fn test_is_server_error_boundary_499() {
514		// Arrange / Act / Assert
515		assert!(!make_response(499, b"").is_server_error());
516	}
517
518	// ========================================================================
519	// content_type and header
520	// ========================================================================
521
522	#[rstest]
523	fn test_content_type() {
524		// Arrange
525		let mut headers = HeaderMap::new();
526		headers.insert("content-type", "application/json".parse().unwrap());
527		let resp = TestResponse::with_body(StatusCode::OK, headers, Bytes::new());
528
529		// Act / Assert
530		assert_eq!(resp.content_type(), Some("application/json"));
531	}
532
533	#[rstest]
534	fn test_content_type_absent() {
535		// Arrange
536		let resp = make_response(200, b"");
537
538		// Act / Assert
539		assert_eq!(resp.content_type(), None);
540	}
541
542	#[rstest]
543	fn test_header_present() {
544		// Arrange
545		let mut headers = HeaderMap::new();
546		headers.insert("x-request-id", "abc123".parse().unwrap());
547		let resp = TestResponse::with_body(StatusCode::OK, headers, Bytes::new());
548
549		// Act / Assert
550		assert_eq!(resp.header("x-request-id"), Some("abc123"));
551	}
552
553	#[rstest]
554	fn test_header_absent() {
555		// Arrange
556		let resp = make_response(200, b"");
557
558		// Act / Assert
559		assert_eq!(resp.header("x-missing"), None);
560	}
561
562	// ========================================================================
563	// ResponseExt assertions
564	// ========================================================================
565
566	#[rstest]
567	fn test_assert_ok() {
568		// Arrange
569		let resp = make_response(200, b"");
570
571		// Act / Assert (should not panic)
572		resp.assert_ok();
573	}
574
575	#[rstest]
576	fn test_assert_created() {
577		// Arrange
578		let resp = make_response(201, b"");
579
580		// Act / Assert
581		resp.assert_created();
582	}
583
584	#[rstest]
585	fn test_assert_no_content() {
586		// Arrange
587		let resp = make_response(204, b"");
588
589		// Act / Assert
590		resp.assert_no_content();
591	}
592
593	#[rstest]
594	fn test_assert_bad_request() {
595		// Arrange
596		let resp = make_response(400, b"");
597
598		// Act / Assert
599		resp.assert_bad_request();
600	}
601
602	#[rstest]
603	fn test_assert_unauthorized() {
604		// Arrange
605		let resp = make_response(401, b"");
606
607		// Act / Assert
608		resp.assert_unauthorized();
609	}
610
611	#[rstest]
612	fn test_assert_forbidden() {
613		// Arrange
614		let resp = make_response(403, b"");
615
616		// Act / Assert
617		resp.assert_forbidden();
618	}
619
620	#[rstest]
621	fn test_assert_not_found() {
622		// Arrange
623		let resp = make_response(404, b"");
624
625		// Act / Assert
626		resp.assert_not_found();
627	}
628
629	#[rstest]
630	fn test_assert_success() {
631		// Arrange
632		let resp = make_response(200, b"");
633
634		// Act / Assert
635		resp.assert_success();
636	}
637
638	#[rstest]
639	fn test_assert_client_error() {
640		// Arrange
641		let resp = make_response(404, b"");
642
643		// Act / Assert
644		resp.assert_client_error();
645	}
646
647	#[rstest]
648	fn test_assert_server_error() {
649		// Arrange
650		let resp = make_response(500, b"");
651
652		// Act / Assert
653		resp.assert_server_error();
654	}
655
656	#[rstest]
657	fn test_fluent_chaining() {
658		// Arrange
659		let resp = make_response(200, b"");
660
661		// Act / Assert - fluent chaining should return &Self
662		resp.assert_ok().assert_success();
663	}
664
665	#[rstest]
666	#[should_panic(expected = "Expected status")]
667	fn test_assert_status_mismatch() {
668		// Arrange
669		let resp = make_response(200, b"");
670
671		// Act (should panic)
672		resp.assert_status(StatusCode::NOT_FOUND);
673	}
674}