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// =========================================================================
178// HELPER FUNCTIONS
179// =========================================================================
180
181/// Check if an error message contains a substring.
182pub fn error_contains(error: &ForgeError, substring: &str) -> bool {
183    error.to_string().contains(substring)
184}
185
186/// Check if a validation error contains a specific field.
187pub fn validation_error_for_field(error: &ForgeError, field: &str) -> bool {
188    match error {
189        ForgeError::Validation(msg) => msg.contains(field),
190        _ => false,
191    }
192}
193
194/// Assert that a value matches a JSON pattern (partial matching).
195///
196/// The pattern only needs to contain the fields you want to verify.
197/// Extra fields in the actual value are ignored.
198///
199/// # Example
200///
201/// ```ignore
202/// let actual = json!({"id": 123, "name": "Test", "extra": "ignored"});
203/// assert!(assert_json_matches(&actual, &json!({"id": 123})));
204/// assert!(assert_json_matches(&actual, &json!({"name": "Test"})));
205/// ```
206pub fn assert_json_matches(actual: &serde_json::Value, pattern: &serde_json::Value) -> bool {
207    match (actual, pattern) {
208        (serde_json::Value::Object(a), serde_json::Value::Object(p)) => {
209            for (key, expected_value) in p {
210                match a.get(key) {
211                    Some(actual_value) => {
212                        if !assert_json_matches(actual_value, expected_value) {
213                            return false;
214                        }
215                    }
216                    None => return false,
217                }
218            }
219            true
220        }
221        (serde_json::Value::Array(a), serde_json::Value::Array(p)) => {
222            if a.len() != p.len() {
223                return false;
224            }
225            a.iter()
226                .zip(p.iter())
227                .all(|(a, p)| assert_json_matches(a, p))
228        }
229        (a, p) => a == p,
230    }
231}
232
233/// Assert that an array contains an element matching a predicate.
234pub fn assert_contains<T, F>(items: &[T], predicate: F) -> bool
235where
236    F: Fn(&T) -> bool,
237{
238    items.iter().any(predicate)
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use crate::error::ForgeError;
245
246    #[test]
247    fn test_assert_ok_macro() {
248        let result: Result<i32, String> = Ok(42);
249        assert_ok!(result);
250    }
251
252    #[test]
253    #[should_panic(expected = "expected Ok")]
254    fn test_assert_ok_macro_fails() {
255        let result: Result<i32, String> = Err("error".to_string());
256        assert_ok!(result);
257    }
258
259    #[test]
260    fn test_assert_err_macro() {
261        let result: Result<i32, String> = Err("error".to_string());
262        assert_err!(result);
263    }
264
265    #[test]
266    #[should_panic(expected = "expected Err")]
267    fn test_assert_err_macro_fails() {
268        let result: Result<i32, String> = Ok(42);
269        assert_err!(result);
270    }
271
272    #[test]
273    fn test_error_contains() {
274        let error = ForgeError::Validation("email is required".to_string());
275        assert!(error_contains(&error, "email"));
276        assert!(error_contains(&error, "required"));
277        assert!(!error_contains(&error, "password"));
278    }
279
280    #[test]
281    fn test_validation_error_for_field() {
282        let error = ForgeError::Validation("email: is invalid".to_string());
283        assert!(validation_error_for_field(&error, "email"));
284        assert!(!validation_error_for_field(&error, "password"));
285
286        let other_error = ForgeError::Internal("internal error".to_string());
287        assert!(!validation_error_for_field(&other_error, "email"));
288    }
289
290    #[test]
291    fn test_assert_json_matches() {
292        let actual = serde_json::json!({
293            "id": 123,
294            "name": "Test",
295            "nested": {
296                "foo": "bar"
297            }
298        });
299
300        // Partial match
301        assert!(assert_json_matches(
302            &actual,
303            &serde_json::json!({"id": 123})
304        ));
305        assert!(assert_json_matches(
306            &actual,
307            &serde_json::json!({"name": "Test"})
308        ));
309        assert!(assert_json_matches(
310            &actual,
311            &serde_json::json!({"nested": {"foo": "bar"}})
312        ));
313
314        // Non-match
315        assert!(!assert_json_matches(
316            &actual,
317            &serde_json::json!({"id": 456})
318        ));
319        assert!(!assert_json_matches(
320            &actual,
321            &serde_json::json!({"missing": true})
322        ));
323    }
324
325    #[test]
326    fn test_assert_json_matches_arrays() {
327        let actual = serde_json::json!([1, 2, 3]);
328        assert!(assert_json_matches(&actual, &serde_json::json!([1, 2, 3])));
329        assert!(!assert_json_matches(&actual, &serde_json::json!([1, 2])));
330        assert!(!assert_json_matches(&actual, &serde_json::json!([1, 2, 4])));
331    }
332
333    #[test]
334    fn test_assert_contains() {
335        let items = vec![1, 2, 3, 4, 5];
336        assert!(assert_contains(&items, |x| *x == 3));
337        assert!(!assert_contains(&items, |x| *x == 6));
338    }
339}