Skip to main content

reinhardt_testkit/server_fn/
assertions.rs

1//! Server function assertion helpers.
2//!
3//! This module provides assertion utilities for testing server function results,
4//! including success/error checking, status code validation, and content verification.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use reinhardt_testkit::server_fn::assertions::{ServerFnResultAssertions, assert_server_fn_returns};
10//!
11//! let result = my_server_fn(input, &ctx).await;
12//!
13//! // Using trait methods
14//! result.should_be_ok();
15//! result.should_have_value(&expected);
16//!
17//! // Using standalone functions
18//! assert_server_fn_returns(&result, &expected);
19//! ```
20
21#![cfg(native)]
22
23use std::fmt::Debug;
24
25use http::StatusCode;
26
27/// Extension trait for asserting server function results.
28///
29/// This trait provides fluent assertion methods for `Result` types
30/// returned by server functions.
31pub trait ServerFnResultAssertions<T, E> {
32	/// Assert that the result is `Ok` and return the value.
33	fn should_be_ok(&self) -> &T;
34
35	/// Assert that the result is `Err` and return the error.
36	fn should_be_err(&self) -> &E;
37
38	/// Assert that the result is `Ok` and the value equals the expected value.
39	fn should_have_value(&self, expected: &T)
40	where
41		T: PartialEq + Debug;
42
43	/// Assert that the result is `Ok` and the value satisfies a predicate.
44	fn should_satisfy<F>(&self, predicate: F)
45	where
46		F: FnOnce(&T) -> bool;
47}
48
49impl<T: Debug, E: Debug> ServerFnResultAssertions<T, E> for Result<T, E> {
50	fn should_be_ok(&self) -> &T {
51		match self {
52			Ok(value) => value,
53			Err(e) => panic!("Expected Ok result, but got Err: {:?}", e),
54		}
55	}
56
57	fn should_be_err(&self) -> &E {
58		match self {
59			Ok(value) => panic!("Expected Err result, but got Ok: {:?}", value),
60			Err(e) => e,
61		}
62	}
63
64	fn should_have_value(&self, expected: &T)
65	where
66		T: PartialEq + Debug,
67	{
68		let actual = self.should_be_ok();
69		assert_eq!(
70			actual, expected,
71			"Expected value {:?}, but got {:?}",
72			expected, actual
73		);
74	}
75
76	fn should_satisfy<F>(&self, predicate: F)
77	where
78		F: FnOnce(&T) -> bool,
79	{
80		let value = self.should_be_ok();
81		assert!(
82			predicate(value),
83			"Value {:?} did not satisfy the predicate",
84			value
85		);
86	}
87}
88
89/// Extension trait for error assertions.
90pub trait ServerFnErrorAssertions<E> {
91	/// Assert that the error message contains the specified text.
92	fn should_contain_message(&self, expected: &str)
93	where
94		E: std::fmt::Display;
95
96	/// Assert that the error message matches exactly.
97	fn should_have_message(&self, expected: &str)
98	where
99		E: std::fmt::Display;
100}
101
102impl<E: Debug> ServerFnErrorAssertions<E> for E {
103	fn should_contain_message(&self, expected: &str)
104	where
105		E: std::fmt::Display,
106	{
107		let message = self.to_string();
108		assert!(
109			message.contains(expected),
110			"Expected error message to contain '{}', but got '{}'",
111			expected,
112			message
113		);
114	}
115
116	fn should_have_message(&self, expected: &str)
117	where
118		E: std::fmt::Display,
119	{
120		let message = self.to_string();
121		assert_eq!(
122			message, expected,
123			"Expected error message '{}', but got '{}'",
124			expected, message
125		);
126	}
127}
128
129/// Assert that a server function returns a specific value.
130pub fn assert_server_fn_returns<T, E>(result: &Result<T, E>, expected: &T)
131where
132	T: PartialEq + Debug,
133	E: Debug,
134{
135	result.should_have_value(expected);
136}
137
138/// Assert that a server function returns an error.
139pub fn assert_server_fn_error<T, E>(result: &Result<T, E>)
140where
141	T: Debug,
142	E: Debug,
143{
144	let _ = result.should_be_err();
145}
146
147/// Assert that a server function error contains the specified message.
148pub fn assert_server_fn_error_contains<T, E>(result: &Result<T, E>, message: &str)
149where
150	T: Debug,
151	E: Debug + std::fmt::Display,
152{
153	let error = result.should_be_err();
154	error.should_contain_message(message);
155}
156
157/// Assert that a validation error occurred for a specific field.
158///
159/// This checks if the error message mentions the specified field name,
160/// which is a common pattern for validation errors.
161pub fn assert_validation_error<T, E>(result: &Result<T, E>, field: &str)
162where
163	T: Debug,
164	E: Debug + std::fmt::Display,
165{
166	let error = result.should_be_err();
167	let message = error.to_string();
168	assert!(
169		message.to_lowercase().contains(&field.to_lowercase()),
170		"Expected validation error for field '{}', but got: {}",
171		field,
172		message
173	);
174}
175
176/// Assert multiple validation errors occurred for the specified fields.
177pub fn assert_validation_errors<T, E>(result: &Result<T, E>, fields: &[&str])
178where
179	T: Debug,
180	E: Debug + std::fmt::Display,
181{
182	let error = result.should_be_err();
183	let message = error.to_string().to_lowercase();
184
185	for field in fields {
186		assert!(
187			message.contains(&field.to_lowercase()),
188			"Expected validation error for field '{}', but it was not found in: {}",
189			field,
190			error
191		);
192	}
193}
194
195/// Response assertion builder for server functions that return HTTP-like responses.
196#[derive(Debug)]
197pub struct ResponseAssertion<T> {
198	value: T,
199	status: Option<StatusCode>,
200}
201
202impl<T> ResponseAssertion<T> {
203	/// Create a new response assertion.
204	pub fn new(value: T) -> Self {
205		Self {
206			value,
207			status: None,
208		}
209	}
210
211	/// Set the expected status code.
212	pub fn with_status(mut self, status: StatusCode) -> Self {
213		self.status = Some(status);
214		self
215	}
216
217	/// Get the underlying value.
218	pub fn value(&self) -> &T {
219		&self.value
220	}
221
222	/// Consume and return the value.
223	pub fn into_value(self) -> T {
224		self.value
225	}
226}
227
228/// Trait for types that can provide a status code.
229pub trait HasStatusCode {
230	/// Get the status code.
231	fn status_code(&self) -> StatusCode;
232}
233
234impl<T: HasStatusCode> ResponseAssertion<T> {
235	/// Assert that the status code matches.
236	pub fn should_have_status(&self, expected: StatusCode) {
237		let actual = self.value.status_code();
238		assert_eq!(
239			actual, expected,
240			"Expected status code {:?}, but got {:?}",
241			expected, actual
242		);
243	}
244
245	/// Assert that the response is successful (2xx).
246	pub fn should_be_success(&self) {
247		let status = self.value.status_code();
248		assert!(
249			status.is_success(),
250			"Expected successful response, but got {:?}",
251			status
252		);
253	}
254
255	/// Assert that the response is a client error (4xx).
256	pub fn should_be_client_error(&self) {
257		let status = self.value.status_code();
258		assert!(
259			status.is_client_error(),
260			"Expected client error response, but got {:?}",
261			status
262		);
263	}
264
265	/// Assert that the response is a server error (5xx).
266	pub fn should_be_server_error(&self) {
267		let status = self.value.status_code();
268		assert!(
269			status.is_server_error(),
270			"Expected server error response, but got {:?}",
271			status
272		);
273	}
274}
275
276/// Macro for creating server function test cases.
277///
278/// This macro provides a convenient way to define multiple test cases
279/// for a server function with different inputs and expected outputs.
280///
281/// # Example
282///
283/// ```rust,ignore
284/// server_fn_test_cases! {
285///     my_server_fn,
286///     // Test case name, input, expected pattern
287///     test_success: (Input { value: 1 }, Ok(_)),
288///     test_error: (Input { value: -1 }, Err(_)),
289/// }
290/// ```
291#[macro_export]
292macro_rules! server_fn_test_cases {
293	($fn_name:path, $($test_name:ident: ($input:expr, $expected:pat)),* $(,)?) => {
294		$(
295			#[rstest::rstest]
296			#[tokio::test]
297			async fn $test_name(
298				// Fixture injected by rstest; may not be directly used in test body
299				#[allow(unused_variables)]
300				server_fn_context: $crate::server_fn::ServerFnTestEnv,
301			) {
302				let result = $fn_name($input, &server_fn_context).await;
303				assert!(
304					matches!(result, $expected),
305					"Expected {:?} to match pattern {}",
306					result,
307					stringify!($expected)
308				);
309			}
310		)*
311	};
312}
313
314/// Assertion module for common HTTP status code checks.
315pub mod assert_status {
316	use super::*;
317
318	/// Assert OK (200) status.
319	pub fn ok<T: HasStatusCode>(response: &T) {
320		let status = response.status_code();
321		assert_eq!(
322			status,
323			StatusCode::OK,
324			"Expected 200 OK, but got {:?}",
325			status
326		);
327	}
328
329	/// Assert Created (201) status.
330	pub fn created<T: HasStatusCode>(response: &T) {
331		let status = response.status_code();
332		assert_eq!(
333			status,
334			StatusCode::CREATED,
335			"Expected 201 Created, but got {:?}",
336			status
337		);
338	}
339
340	/// Assert No Content (204) status.
341	pub fn no_content<T: HasStatusCode>(response: &T) {
342		let status = response.status_code();
343		assert_eq!(
344			status,
345			StatusCode::NO_CONTENT,
346			"Expected 204 No Content, but got {:?}",
347			status
348		);
349	}
350
351	/// Assert Bad Request (400) status.
352	pub fn bad_request<T: HasStatusCode>(response: &T) {
353		let status = response.status_code();
354		assert_eq!(
355			status,
356			StatusCode::BAD_REQUEST,
357			"Expected 400 Bad Request, but got {:?}",
358			status
359		);
360	}
361
362	/// Assert Unauthorized (401) status.
363	pub fn unauthorized<T: HasStatusCode>(response: &T) {
364		let status = response.status_code();
365		assert_eq!(
366			status,
367			StatusCode::UNAUTHORIZED,
368			"Expected 401 Unauthorized, but got {:?}",
369			status
370		);
371	}
372
373	/// Assert Forbidden (403) status.
374	pub fn forbidden<T: HasStatusCode>(response: &T) {
375		let status = response.status_code();
376		assert_eq!(
377			status,
378			StatusCode::FORBIDDEN,
379			"Expected 403 Forbidden, but got {:?}",
380			status
381		);
382	}
383
384	/// Assert Not Found (404) status.
385	pub fn not_found<T: HasStatusCode>(response: &T) {
386		let status = response.status_code();
387		assert_eq!(
388			status,
389			StatusCode::NOT_FOUND,
390			"Expected 404 Not Found, but got {:?}",
391			status
392		);
393	}
394
395	/// Assert Conflict (409) status.
396	pub fn conflict<T: HasStatusCode>(response: &T) {
397		let status = response.status_code();
398		assert_eq!(
399			status,
400			StatusCode::CONFLICT,
401			"Expected 409 Conflict, but got {:?}",
402			status
403		);
404	}
405
406	/// Assert Unprocessable Entity (422) status.
407	pub fn unprocessable_entity<T: HasStatusCode>(response: &T) {
408		let status = response.status_code();
409		assert_eq!(
410			status,
411			StatusCode::UNPROCESSABLE_ENTITY,
412			"Expected 422 Unprocessable Entity, but got {:?}",
413			status
414		);
415	}
416
417	/// Assert Internal Server Error (500) status.
418	pub fn internal_error<T: HasStatusCode>(response: &T) {
419		let status = response.status_code();
420		assert_eq!(
421			status,
422			StatusCode::INTERNAL_SERVER_ERROR,
423			"Expected 500 Internal Server Error, but got {:?}",
424			status
425		);
426	}
427}
428
429#[cfg(test)]
430mod tests {
431	use super::*;
432	use rstest::rstest;
433
434	#[derive(Debug)]
435	struct StatusResponse(StatusCode);
436
437	impl HasStatusCode for StatusResponse {
438		fn status_code(&self) -> StatusCode {
439			self.0
440		}
441	}
442
443	#[test]
444	fn test_should_be_ok() {
445		let result: Result<i32, &str> = Ok(42);
446		assert_eq!(*result.should_be_ok(), 42);
447	}
448
449	#[test]
450	#[should_panic(expected = "Expected Ok result")]
451	fn test_should_be_ok_panics_on_err() {
452		let result: Result<i32, &str> = Err("error");
453		result.should_be_ok();
454	}
455
456	#[test]
457	fn test_should_be_err() {
458		let result: Result<i32, &str> = Err("error");
459		assert_eq!(*result.should_be_err(), "error");
460	}
461
462	#[test]
463	#[should_panic(expected = "Expected Err result")]
464	fn test_should_be_err_panics_on_ok() {
465		let result: Result<i32, &str> = Ok(42);
466		result.should_be_err();
467	}
468
469	#[test]
470	fn test_should_have_value() {
471		let result: Result<i32, &str> = Ok(42);
472		result.should_have_value(&42);
473	}
474
475	#[test]
476	fn test_should_satisfy() {
477		let result: Result<i32, &str> = Ok(42);
478		result.should_satisfy(|v| *v > 0);
479	}
480
481	#[test]
482	fn test_assert_server_fn_returns() {
483		let result: Result<i32, &str> = Ok(42);
484		assert_server_fn_returns(&result, &42);
485	}
486
487	#[test]
488	fn test_assert_server_fn_error_contains() {
489		let result: Result<i32, String> = Err("Validation failed: email is required".to_string());
490		assert_server_fn_error_contains(&result, "email");
491	}
492
493	#[test]
494	fn test_assert_validation_error() {
495		let result: Result<i32, String> = Err("Field 'username' is required".to_string());
496		assert_validation_error(&result, "username");
497	}
498
499	#[test]
500	fn test_assert_validation_errors() {
501		let result: Result<i32, String> =
502			Err("Validation errors: username is required, email is invalid".to_string());
503		assert_validation_errors(&result, &["username", "email"]);
504	}
505
506	#[rstest]
507	fn server_fn_result_and_response_assertions_cover_public_success_paths() {
508		// Arrange
509		let successful: Result<&str, String> = Ok("created");
510		let failing: Result<&str, String> = Err("email and username are invalid".to_string());
511		let ok = StatusResponse(StatusCode::OK);
512		let created = StatusResponse(StatusCode::CREATED);
513		let empty = StatusResponse(StatusCode::NO_CONTENT);
514		let bad_request = StatusResponse(StatusCode::BAD_REQUEST);
515		let unauthorized = StatusResponse(StatusCode::UNAUTHORIZED);
516		let forbidden = StatusResponse(StatusCode::FORBIDDEN);
517		let missing = StatusResponse(StatusCode::NOT_FOUND);
518		let conflict = StatusResponse(StatusCode::CONFLICT);
519		let invalid = StatusResponse(StatusCode::UNPROCESSABLE_ENTITY);
520		let internal = StatusResponse(StatusCode::INTERNAL_SERVER_ERROR);
521
522		// Act
523		assert_server_fn_returns(&successful, &"created");
524		let return_value_mismatch = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
525			assert_server_fn_returns(&successful, &"updated");
526		}));
527		assert_server_fn_error(&failing);
528		let successful_error_assertion =
529			std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
530				assert_server_fn_error(&successful);
531			}));
532		assert_server_fn_error_contains(&failing, "username");
533		let missing_substring = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
534			assert_server_fn_error_contains(&failing, "password");
535		}));
536		assert_validation_error(&failing, "EMAIL");
537		assert_validation_errors(&failing, &["email", "username"]);
538		let missing_validation_field =
539			std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
540				assert_validation_error(&failing, "password");
541			}));
542		let missing_validation_member =
543			std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
544				assert_validation_errors(&failing, &["email", "password"]);
545			}));
546		failing
547			.should_be_err()
548			.should_have_message("email and username are invalid");
549		let message_mismatch = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
550			failing
551				.should_be_err()
552				.should_have_message("email and password are invalid");
553		}));
554		let assertion = ResponseAssertion::new(created).with_status(StatusCode::CREATED);
555		assert_eq!(assertion.status, Some(StatusCode::CREATED));
556		assertion.should_have_status(StatusCode::CREATED);
557		assertion.should_be_success();
558		let non_success = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
559			ResponseAssertion::new(StatusResponse(StatusCode::BAD_REQUEST)).should_be_success();
560		}));
561		let extracted = assertion.into_value();
562
563		// Assert
564		assert_eq!(extracted.status_code(), StatusCode::CREATED);
565		assert_eq!(
566			ResponseAssertion::new(ok).value().status_code(),
567			StatusCode::OK
568		);
569		assert_status::ok(&StatusResponse(StatusCode::OK));
570		assert_status::created(&StatusResponse(StatusCode::CREATED));
571		assert_status::no_content(&empty);
572		assert_status::bad_request(&bad_request);
573		assert_status::unauthorized(&unauthorized);
574		assert_status::forbidden(&forbidden);
575		assert_status::not_found(&missing);
576		assert_status::conflict(&conflict);
577		assert_status::unprocessable_entity(&invalid);
578		assert_status::internal_error(&internal);
579		ResponseAssertion::new(bad_request).should_be_client_error();
580		ResponseAssertion::new(internal).should_be_server_error();
581		assert!(successful_error_assertion.is_err());
582		assert!(missing_substring.is_err());
583		assert!(return_value_mismatch.is_err());
584		assert!(message_mismatch.is_err());
585		assert!(missing_validation_field.is_err());
586		assert!(missing_validation_member.is_err());
587		assert!(non_success.is_err());
588	}
589
590	fn assert_status_panics(name: &str, assertion: impl FnOnce()) {
591		assert!(
592			std::panic::catch_unwind(std::panic::AssertUnwindSafe(assertion)).is_err(),
593			"{name} accepted a mismatched status"
594		);
595	}
596
597	#[rstest]
598	fn status_assertions_reject_mismatched_statuses() {
599		// Arrange
600		let ok = StatusResponse(StatusCode::OK);
601		let created = StatusResponse(StatusCode::CREATED);
602		let bad_request = StatusResponse(StatusCode::BAD_REQUEST);
603		let internal = StatusResponse(StatusCode::INTERNAL_SERVER_ERROR);
604
605		// Act and assert
606		assert_status_panics("ok", || assert_status::ok(&created));
607		assert_status_panics("created", || assert_status::created(&ok));
608		assert_status_panics("no_content", || assert_status::no_content(&ok));
609		assert_status_panics("bad_request", || assert_status::bad_request(&ok));
610		assert_status_panics("unauthorized", || assert_status::unauthorized(&ok));
611		assert_status_panics("forbidden", || assert_status::forbidden(&ok));
612		assert_status_panics("not_found", || assert_status::not_found(&ok));
613		assert_status_panics("conflict", || assert_status::conflict(&ok));
614		assert_status_panics("unprocessable_entity", || {
615			assert_status::unprocessable_entity(&ok)
616		});
617		assert_status_panics("internal_error", || assert_status::internal_error(&ok));
618		assert_status_panics("client_error_category", || {
619			ResponseAssertion::new(internal).should_be_client_error()
620		});
621		assert_status_panics("server_error_category", || {
622			ResponseAssertion::new(bad_request).should_be_server_error()
623		});
624	}
625}