Skip to main content

forge_core/testing/
assertions.rs

1//! Test assertion macros and helpers.
2//!
3//! Provides ergonomic assertion macros for common FORGE testing patterns.
4
5use crate::error::ForgeError;
6
7/// Assert that a result is Ok.
8///
9/// # Example
10///
11/// ```ignore
12/// let result = some_operation();
13/// assert_ok!(result);
14/// assert_ok!(result, "Operation should succeed");
15/// ```
16#[macro_export]
17macro_rules! assert_ok {
18    ($expr:expr) => {
19        match &$expr {
20            Ok(_) => (),
21            Err(e) => panic!("assertion failed: expected Ok, got Err({:?})", e),
22        }
23    };
24    ($expr:expr, $($arg:tt)+) => {
25        match &$expr {
26            Ok(_) => (),
27            Err(e) => panic!("assertion failed: {}: expected Ok, got Err({:?})", format_args!($($arg)+), e),
28        }
29    };
30}
31
32/// Assert that a result is Err.
33///
34/// # Example
35///
36/// ```ignore
37/// let result: Result<(), ForgeError> = Err(ForgeError::Unauthorized);
38/// assert_err!(result);
39/// ```
40#[macro_export]
41macro_rules! assert_err {
42    ($expr:expr) => {
43        match &$expr {
44            Err(_) => (),
45            Ok(v) => panic!("assertion failed: expected Err, got Ok({:?})", v),
46        }
47    };
48    ($expr:expr, $($arg:tt)+) => {
49        match &$expr {
50            Err(_) => (),
51            Ok(v) => panic!("assertion failed: {}: expected Err, got Ok({:?})", format_args!($($arg)+), v),
52        }
53    };
54}
55
56/// Assert that an error matches a specific variant.
57///
58/// # Example
59///
60/// ```ignore
61/// let result: Result<(), ForgeError> = Err(ForgeError::NotFound("user".into()));
62/// assert_err_variant!(result, ForgeError::NotFound(_));
63/// ```
64#[macro_export]
65macro_rules! assert_err_variant {
66    ($expr:expr, $variant:pat) => {
67        match &$expr {
68            Err($variant) => (),
69            Err(e) => panic!(
70                "assertion failed: expected {}, got {:?}",
71                stringify!($variant),
72                e
73            ),
74            Ok(v) => panic!(
75                "assertion failed: expected Err({}), got Ok({:?})",
76                stringify!($variant),
77                v
78            ),
79        }
80    };
81}
82
83/// Assert that a job was dispatched.
84///
85/// # Example
86///
87/// ```ignore
88/// assert_job_dispatched!(ctx, "send_email");
89/// assert_job_dispatched!(ctx, "send_email", |args| args["to"] == "test@example.com");
90/// ```
91#[macro_export]
92macro_rules! assert_job_dispatched {
93    ($ctx:expr, $job_type:expr) => {
94        $ctx.job_dispatch().assert_dispatched($job_type);
95    };
96    ($ctx:expr, $job_type:expr, $predicate:expr) => {
97        $ctx.job_dispatch()
98            .assert_dispatched_with($job_type, $predicate);
99    };
100}
101
102/// Assert that a job was not dispatched.
103///
104/// # Example
105///
106/// ```ignore
107/// assert_job_not_dispatched!(ctx, "send_email");
108/// ```
109#[macro_export]
110macro_rules! assert_job_not_dispatched {
111    ($ctx:expr, $job_type:expr) => {
112        $ctx.job_dispatch().assert_not_dispatched($job_type);
113    };
114}
115
116/// Assert that a workflow was started.
117///
118/// # Example
119///
120/// ```ignore
121/// assert_workflow_started!(ctx, "onboarding");
122/// assert_workflow_started!(ctx, "onboarding", |input| input["user_id"] == "123");
123/// ```
124#[macro_export]
125macro_rules! assert_workflow_started {
126    ($ctx:expr, $workflow_name:expr) => {
127        $ctx.workflow_dispatch().assert_started($workflow_name);
128    };
129    ($ctx:expr, $workflow_name:expr, $predicate:expr) => {
130        $ctx.workflow_dispatch()
131            .assert_started_with($workflow_name, $predicate);
132    };
133}
134
135/// Assert that a workflow was not started.
136///
137/// # Example
138///
139/// ```ignore
140/// assert_workflow_not_started!(ctx, "onboarding");
141/// ```
142#[macro_export]
143macro_rules! assert_workflow_not_started {
144    ($ctx:expr, $workflow_name:expr) => {
145        $ctx.workflow_dispatch().assert_not_started($workflow_name);
146    };
147}
148
149/// Assert that an HTTP call was made.
150///
151/// # Example
152///
153/// ```ignore
154/// assert_http_called!(ctx, "https://api.example.com/*");
155/// ```
156#[macro_export]
157macro_rules! assert_http_called {
158    ($ctx:expr, $pattern:expr) => {
159        $ctx.http().assert_called($pattern);
160    };
161}
162
163/// Assert that an HTTP call was not made.
164///
165/// # Example
166///
167/// ```ignore
168/// assert_http_not_called!(ctx, "https://api.example.com/*");
169/// ```
170#[macro_export]
171macro_rules! assert_http_not_called {
172    ($ctx:expr, $pattern:expr) => {
173        $ctx.http().assert_not_called($pattern);
174    };
175}
176
177/// Check if an error message contains a substring.
178pub fn error_contains(error: &ForgeError, substring: &str) -> bool {
179    error.to_string().contains(substring)
180}
181
182/// Check if a validation error contains a specific field.
183pub fn validation_error_for_field(error: &ForgeError, field: &str) -> bool {
184    match error {
185        ForgeError::Validation(msg) => msg.contains(field),
186        _ => false,
187    }
188}
189
190/// Assert that a value matches a JSON pattern (partial matching).
191///
192/// The pattern only needs to contain the fields you want to verify.
193/// Extra fields in the actual value are ignored.
194///
195/// # Example
196///
197/// ```ignore
198/// let actual = json!({"id": 123, "name": "Test", "extra": "ignored"});
199/// assert!(assert_json_matches(&actual, &json!({"id": 123})));
200/// assert!(assert_json_matches(&actual, &json!({"name": "Test"})));
201/// ```
202pub fn assert_json_matches(actual: &serde_json::Value, pattern: &serde_json::Value) -> bool {
203    match (actual, pattern) {
204        (serde_json::Value::Object(a), serde_json::Value::Object(p)) => {
205            for (key, expected_value) in p {
206                match a.get(key) {
207                    Some(actual_value) => {
208                        if !assert_json_matches(actual_value, expected_value) {
209                            return false;
210                        }
211                    }
212                    None => return false,
213                }
214            }
215            true
216        }
217        (serde_json::Value::Array(a), serde_json::Value::Array(p)) => {
218            if a.len() != p.len() {
219                return false;
220            }
221            a.iter()
222                .zip(p.iter())
223                .all(|(a, p)| assert_json_matches(a, p))
224        }
225        (a, p) => a == p,
226    }
227}
228
229/// Assert that an array contains an element matching a predicate.
230pub fn assert_contains<T, F>(items: &[T], predicate: F) -> bool
231where
232    F: Fn(&T) -> bool,
233{
234    items.iter().any(predicate)
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::error::ForgeError;
241
242    #[test]
243    fn test_assert_ok_macro() {
244        let result: Result<i32, String> = Ok(42);
245        assert_ok!(result);
246    }
247
248    #[test]
249    #[should_panic(expected = "expected Ok")]
250    fn test_assert_ok_macro_fails() {
251        let result: Result<i32, String> = Err("error".to_string());
252        assert_ok!(result);
253    }
254
255    #[test]
256    fn test_assert_err_macro() {
257        let result: Result<i32, String> = Err("error".to_string());
258        assert_err!(result);
259    }
260
261    #[test]
262    #[should_panic(expected = "expected Err")]
263    fn test_assert_err_macro_fails() {
264        let result: Result<i32, String> = Ok(42);
265        assert_err!(result);
266    }
267
268    #[test]
269    fn test_error_contains() {
270        let error = ForgeError::Validation("email is required".to_string());
271        assert!(error_contains(&error, "email"));
272        assert!(error_contains(&error, "required"));
273        assert!(!error_contains(&error, "password"));
274    }
275
276    #[test]
277    fn test_validation_error_for_field() {
278        let error = ForgeError::Validation("email: is invalid".to_string());
279        assert!(validation_error_for_field(&error, "email"));
280        assert!(!validation_error_for_field(&error, "password"));
281
282        let other_error = ForgeError::Internal("internal error".to_string());
283        assert!(!validation_error_for_field(&other_error, "email"));
284    }
285
286    #[test]
287    fn test_assert_json_matches() {
288        let actual = serde_json::json!({
289            "id": 123,
290            "name": "Test",
291            "nested": {
292                "foo": "bar"
293            }
294        });
295
296        // Partial match
297        assert!(assert_json_matches(
298            &actual,
299            &serde_json::json!({"id": 123})
300        ));
301        assert!(assert_json_matches(
302            &actual,
303            &serde_json::json!({"name": "Test"})
304        ));
305        assert!(assert_json_matches(
306            &actual,
307            &serde_json::json!({"nested": {"foo": "bar"}})
308        ));
309
310        // Non-match
311        assert!(!assert_json_matches(
312            &actual,
313            &serde_json::json!({"id": 456})
314        ));
315        assert!(!assert_json_matches(
316            &actual,
317            &serde_json::json!({"missing": true})
318        ));
319    }
320
321    #[test]
322    fn test_assert_json_matches_arrays() {
323        let actual = serde_json::json!([1, 2, 3]);
324        assert!(assert_json_matches(&actual, &serde_json::json!([1, 2, 3])));
325        assert!(!assert_json_matches(&actual, &serde_json::json!([1, 2])));
326        assert!(!assert_json_matches(&actual, &serde_json::json!([1, 2, 4])));
327    }
328
329    #[test]
330    fn test_assert_contains() {
331        let items = vec![1, 2, 3, 4, 5];
332        assert!(assert_contains(&items, |x| *x == 3));
333        assert!(!assert_contains(&items, |x| *x == 6));
334    }
335}