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(not(target_arch = "wasm32"))]
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
433	#[test]
434	fn test_should_be_ok() {
435		let result: Result<i32, &str> = Ok(42);
436		assert_eq!(*result.should_be_ok(), 42);
437	}
438
439	#[test]
440	#[should_panic(expected = "Expected Ok result")]
441	fn test_should_be_ok_panics_on_err() {
442		let result: Result<i32, &str> = Err("error");
443		result.should_be_ok();
444	}
445
446	#[test]
447	fn test_should_be_err() {
448		let result: Result<i32, &str> = Err("error");
449		assert_eq!(*result.should_be_err(), "error");
450	}
451
452	#[test]
453	#[should_panic(expected = "Expected Err result")]
454	fn test_should_be_err_panics_on_ok() {
455		let result: Result<i32, &str> = Ok(42);
456		result.should_be_err();
457	}
458
459	#[test]
460	fn test_should_have_value() {
461		let result: Result<i32, &str> = Ok(42);
462		result.should_have_value(&42);
463	}
464
465	#[test]
466	fn test_should_satisfy() {
467		let result: Result<i32, &str> = Ok(42);
468		result.should_satisfy(|v| *v > 0);
469	}
470
471	#[test]
472	fn test_assert_server_fn_returns() {
473		let result: Result<i32, &str> = Ok(42);
474		assert_server_fn_returns(&result, &42);
475	}
476
477	#[test]
478	fn test_assert_server_fn_error_contains() {
479		let result: Result<i32, String> = Err("Validation failed: email is required".to_string());
480		assert_server_fn_error_contains(&result, "email");
481	}
482
483	#[test]
484	fn test_assert_validation_error() {
485		let result: Result<i32, String> = Err("Field 'username' is required".to_string());
486		assert_validation_error(&result, "username");
487	}
488
489	#[test]
490	fn test_assert_validation_errors() {
491		let result: Result<i32, String> =
492			Err("Validation errors: username is required, email is invalid".to_string());
493		assert_validation_errors(&result, &["username", "email"]);
494	}
495}