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