1#![cfg(native)]
22
23use std::fmt::Debug;
24
25use http::StatusCode;
26
27pub trait ServerFnResultAssertions<T, E> {
32 fn should_be_ok(&self) -> &T;
34
35 fn should_be_err(&self) -> &E;
37
38 fn should_have_value(&self, expected: &T)
40 where
41 T: PartialEq + Debug;
42
43 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
89pub trait ServerFnErrorAssertions<E> {
91 fn should_contain_message(&self, expected: &str)
93 where
94 E: std::fmt::Display;
95
96 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
129pub 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
138pub 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
147pub 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
157pub 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
176pub 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#[derive(Debug)]
197pub struct ResponseAssertion<T> {
198 value: T,
199 status: Option<StatusCode>,
200}
201
202impl<T> ResponseAssertion<T> {
203 pub fn new(value: T) -> Self {
205 Self {
206 value,
207 status: None,
208 }
209 }
210
211 pub fn with_status(mut self, status: StatusCode) -> Self {
213 self.status = Some(status);
214 self
215 }
216
217 pub fn value(&self) -> &T {
219 &self.value
220 }
221
222 pub fn into_value(self) -> T {
224 self.value
225 }
226}
227
228pub trait HasStatusCode {
230 fn status_code(&self) -> StatusCode;
232}
233
234impl<T: HasStatusCode> ResponseAssertion<T> {
235 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 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 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 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_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 #[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
314pub mod assert_status {
316 use super::*;
317
318 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 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 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 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 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 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 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 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 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 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 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 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_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 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 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}