Skip to main content

klauthed_testing/
assertions.rs

1//! Terse assertions for [`DomainError`] values.
2//!
3//! Error tests otherwise repeat `assert_eq!(err.category(), ...)` and
4//! `assert_eq!(err.code().as_str(), ...)`. These helpers — both free functions
5//! and a [`DomainErrorExt`] extension trait — make the intent obvious and the
6//! panic messages descriptive.
7//!
8//! ```
9//! use klauthed_testing::assertions::{assert_category, assert_code, DomainErrorExt};
10//! use klauthed_error::{DomainError, ErrorCategory, ErrorCode};
11//!
12//! #[derive(Debug)]
13//! struct NotThere;
14//! impl std::fmt::Display for NotThere {
15//!     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16//!         f.write_str("missing")
17//!     }
18//! }
19//! impl std::error::Error for NotThere {}
20//! impl DomainError for NotThere {
21//!     fn category(&self) -> ErrorCategory { ErrorCategory::NotFound }
22//!     fn code(&self) -> ErrorCode { ErrorCode::new("thing.not_found") }
23//! }
24//!
25//! let err = NotThere;
26//! // Free functions:
27//! assert_category(&err, ErrorCategory::NotFound);
28//! assert_code(&err, "thing.not_found");
29//! // Or the fluent extension trait:
30//! err.assert_category(ErrorCategory::NotFound)
31//!    .assert_code("thing.not_found")
32//!    .assert_http_status(404);
33//! ```
34
35use klauthed_error::{DomainError, ErrorCategory};
36
37/// Assert that `err`'s [`category`](DomainError::category) equals `expected`.
38///
39/// # Panics
40/// Panics with the actual category and code if they differ.
41#[track_caller]
42pub fn assert_category<E: DomainError + ?Sized>(err: &E, expected: ErrorCategory) {
43    let actual = err.category();
44    assert!(
45        actual == expected,
46        "expected category {expected:?}, got {actual:?} (code: {}, error: {err})",
47        err.code()
48    );
49}
50
51/// Assert that `err`'s [`code`](DomainError::code) string equals `expected`.
52///
53/// # Panics
54/// Panics with the actual code and category if they differ.
55#[track_caller]
56pub fn assert_code<E: DomainError + ?Sized>(err: &E, expected: &str) {
57    let actual = err.code();
58    assert!(
59        actual.as_str() == expected,
60        "expected code '{expected}', got '{actual}' (category: {:?}, error: {err})",
61        err.category()
62    );
63}
64
65/// Assert that `err`'s [`http_status`](DomainError::http_status) equals `expected`.
66///
67/// # Panics
68/// Panics with the actual status if it differs.
69#[track_caller]
70pub fn assert_http_status<E: DomainError + ?Sized>(err: &E, expected: u16) {
71    let actual = err.http_status();
72    assert!(
73        actual == expected,
74        "expected HTTP status {expected}, got {actual} (code: {}, error: {err})",
75        err.code()
76    );
77}
78
79/// Assert that `err`'s [`is_retryable`](DomainError::is_retryable) equals `expected`.
80///
81/// # Panics
82/// Panics if the retryability differs.
83#[track_caller]
84pub fn assert_retryable<E: DomainError + ?Sized>(err: &E, expected: bool) {
85    let actual = err.is_retryable();
86    assert!(
87        actual == expected,
88        "expected is_retryable = {expected}, got {actual} (code: {}, error: {err})",
89        err.code()
90    );
91}
92
93/// Fluent assertions on any [`DomainError`], each returning `&self` so they chain.
94pub trait DomainErrorExt: DomainError {
95    /// Assert this error's category. Returns `&self` for chaining.
96    #[track_caller]
97    fn assert_category(&self, expected: ErrorCategory) -> &Self {
98        assert_category(self, expected);
99        self
100    }
101
102    /// Assert this error's code string. Returns `&self` for chaining.
103    #[track_caller]
104    fn assert_code(&self, expected: &str) -> &Self {
105        assert_code(self, expected);
106        self
107    }
108
109    /// Assert this error's HTTP status. Returns `&self` for chaining.
110    #[track_caller]
111    fn assert_http_status(&self, expected: u16) -> &Self {
112        assert_http_status(self, expected);
113        self
114    }
115
116    /// Assert this error's retryability. Returns `&self` for chaining.
117    #[track_caller]
118    fn assert_retryable(&self, expected: bool) -> &Self {
119        assert_retryable(self, expected);
120        self
121    }
122}
123
124impl<E: DomainError + ?Sized> DomainErrorExt for E {}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use klauthed_error::ErrorCode;
130
131    #[derive(Debug)]
132    struct Sample;
133    impl std::fmt::Display for Sample {
134        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135            f.write_str("sample error")
136        }
137    }
138    impl std::error::Error for Sample {}
139    impl DomainError for Sample {
140        fn category(&self) -> ErrorCategory {
141            ErrorCategory::Unavailable
142        }
143        fn code(&self) -> ErrorCode {
144            ErrorCode::new("sample.down")
145        }
146    }
147
148    #[test]
149    fn free_functions_pass_on_match() {
150        let err = Sample;
151        assert_category(&err, ErrorCategory::Unavailable);
152        assert_code(&err, "sample.down");
153        assert_http_status(&err, 503);
154        assert_retryable(&err, true);
155    }
156
157    #[test]
158    fn extension_trait_chains() {
159        Sample
160            .assert_category(ErrorCategory::Unavailable)
161            .assert_code("sample.down")
162            .assert_http_status(503)
163            .assert_retryable(true);
164    }
165
166    #[test]
167    #[should_panic(expected = "expected category")]
168    fn category_mismatch_panics() {
169        assert_category(&Sample, ErrorCategory::NotFound);
170    }
171
172    #[test]
173    #[should_panic(expected = "expected code")]
174    fn code_mismatch_panics() {
175        assert_code(&Sample, "sample.up");
176    }
177}