expect_json/expect/ops/expect_object/
expect_object.rs

1use crate::expect::ops::expect_object::ExpectObjectSubOp;
2use crate::expect_core::expect_op;
3use crate::expect_core::Context;
4use crate::expect_core::ExpectOp;
5use crate::expect_core::ExpectOpResult;
6use crate::JsonType;
7use serde_json::Map;
8use serde_json::Value;
9
10#[expect_op(internal, name = "object")]
11#[derive(Debug, Clone, Default, PartialEq)]
12pub struct ExpectObject {
13    sub_ops: Vec<ExpectObjectSubOp>,
14}
15
16impl ExpectObject {
17    pub(crate) fn new() -> Self {
18        Self { sub_ops: vec![] }
19    }
20
21    pub fn empty(mut self) -> Self {
22        self.sub_ops.push(ExpectObjectSubOp::Empty);
23        self
24    }
25
26    pub fn not_empty(mut self) -> Self {
27        self.sub_ops.push(ExpectObjectSubOp::NotEmpty);
28        self
29    }
30
31    /// Expect an object containing a subset of the JSON object given.
32    ///
33    /// ```rust
34    /// # async fn test() -> Result<(), Box<dyn ::std::error::Error>> {
35    /// #
36    /// # use axum::Router;
37    /// # use axum::extract::Json;
38    /// # use axum::routing::get;
39    /// # use axum_test::TestServer;
40    /// # use serde_json::json;
41    /// #
42    /// # let server = TestServer::new(Router::new())?;
43    /// #
44    /// use axum_test::expect_json;
45    ///
46    /// let server = TestServer::new(Router::new())?;
47    ///
48    /// server.get(&"/user/barrington")
49    ///     .await
50    ///     .assert_json(&json!({
51    ///         "name": "Barrington",
52    ///         "metadata": expect_json::object().contains(json!({
53    ///             "name": "Joe",
54    ///             "age": expect_json::integer().in_range(18..=100),
55    ///             "email": expect_json::email(),
56    ///         })),
57    ///     }));
58    /// #
59    /// # Ok(()) }
60    /// ```
61    pub fn contains<V>(mut self, expected_values: V) -> Self
62    where
63        V: Into<Value>,
64    {
65        let value = Into::<Value>::into(expected_values);
66        let sub_op = match value {
67            Value::Object(values_object) => ExpectObjectSubOp::Contains(values_object),
68            _ => {
69                let value_type = JsonType::from(&value);
70                panic!("object().contains() expected to take object. Received: {value_type}");
71            }
72        };
73
74        self.sub_ops.push(sub_op);
75        self
76    }
77
78    #[doc(hidden)]
79    pub fn propagated_contains<V>(mut self, expected_values: V) -> Self
80    where
81        V: Into<Value>,
82    {
83        let value = Into::<Value>::into(expected_values);
84        let sub_op = match value {
85            Value::Object(values_object) => ExpectObjectSubOp::PartialContains(values_object),
86            _ => {
87                let value_type = JsonType::from(&value);
88                panic!("object().propagated_contains() expected to take object. Received: {value_type}");
89            }
90        };
91
92        self.sub_ops.push(sub_op);
93        self
94    }
95}
96
97impl ExpectOp for ExpectObject {
98    fn on_object(
99        &self,
100        context: &mut Context,
101        received: &Map<String, Value>,
102    ) -> ExpectOpResult<()> {
103        for sub_op in &self.sub_ops {
104            sub_op.on_object(self, context, received)?;
105        }
106
107        Ok(())
108    }
109
110    fn debug_supported_types(&self) -> &'static [JsonType] {
111        &[JsonType::Object]
112    }
113}
114
115#[cfg(test)]
116mod test_contains {
117    use crate::expect;
118    use crate::expect_json_eq;
119    use pretty_assertions::assert_eq;
120    use serde_json::json;
121
122    #[test]
123    fn it_should_be_equal_for_identical_objects() {
124        let left = json!({ "name": "John", "age": 30, "scores": [1, 2, 3] });
125        let right =
126            json!(expect::object()
127                .contains(json!({ "name": "John", "age": 30, "scores": [1, 2, 3] })));
128
129        let output = expect_json_eq(&left, &right);
130        assert!(output.is_ok());
131    }
132
133    #[test]
134    fn it_should_be_equal_for_reversed_identical_objects() {
135        let left = json!({ "name": "John", "age": 30, "scores": [1, 2, 3] });
136        let right =
137            json!(expect::object()
138                .contains(json!({ "scores": [1, 2, 3], "age": 30, "name": "John" })));
139
140        let output = expect_json_eq(&left, &right);
141        assert!(output.is_ok());
142    }
143
144    #[test]
145    fn it_should_be_equal_for_partial_contains() {
146        let left = json!({ "name": "John", "age": 30, "scores": [1, 2, 3] });
147        let right = json!(expect::object().contains(json!({ "name": "John", "age": 30 })));
148
149        let output = expect_json_eq(&left, &right);
150        assert!(output.is_ok());
151    }
152
153    #[test]
154    fn it_should_be_equal_for_nested_contains() {
155        let left = json!({ "name": "John", "comments": [
156            {
157                "text": "Hello",
158                "author": {
159                    "name": "Jane",
160                    "age": 25
161                }
162            },
163            {
164                "text": "Goodbye",
165                "author": {
166                    "name": "John",
167                    "age": 30
168                }
169            }
170        ]});
171
172        let right = json!(expect::object().contains(
173            json!({ "comments": expect::array().contains([
174                json!({
175                    "text": "Hello",
176                    "author": expect::object().contains(
177                        json!({
178                            "name": "Jane",
179                        })
180                    )
181                }),
182            ])})
183        ));
184
185        let output = expect_json_eq(&left, &right);
186        assert!(output.is_ok(), "{}", output.unwrap_err().to_string());
187    }
188
189    #[test]
190    fn it_should_error_for_same_fields_but_different_values() {
191        let left = json!({ "name": "John", "age": 30, "scores": [1, 2, 3] });
192        let right = json!(
193            expect::object().contains(json!({ "name": "Joe", "age": 31, "scores": [4, 5, 6] }))
194        );
195
196        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
197        assert_eq!(
198            output,
199            r#"Json integers at root.age are not equal:
200    expected 31
201    received 30"#
202        );
203    }
204
205    #[test]
206    fn it_should_be_ok_for_empty_contains() {
207        let left = json!({ "name": "John", "age": 30, "scores": [1, 2, 3] });
208        let right = json!(expect::object().contains(json!({})));
209
210        let output = expect_json_eq(&left, &right);
211        assert!(output.is_ok());
212    }
213
214    #[test]
215    fn it_should_be_ok_for_empty_on_empty_object() {
216        let left = json!({});
217        let right = json!(expect::object().contains(json!({})));
218
219        let output = expect_json_eq(&left, &right);
220        assert!(output.is_ok());
221    }
222
223    #[test]
224    fn it_should_error_if_used_against_the_wrong_type() {
225        let left = json!("🦊");
226        let right = json!(expect::object().contains(json!({})));
227
228        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
229        assert_eq!(
230            output,
231            r#"Json expect::object() at root, received wrong type:
232    expected object
233    received string "🦊""#
234        );
235    }
236
237    #[test]
238    fn it_should_error_if_nested_object_is_missing_fields() {
239        let left = json!({
240            "name": "John",
241            "id": "abd123",
242            "meta": {
243                "location": "uk",
244                "previous_login": "sometime last thursday",
245            }
246        });
247
248        let right = json!(expect::object().contains(json!({
249            "name": "John",
250            "meta": {},
251        })));
252
253        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
254        assert_eq!(
255            output,
256            r#"Json object at root.meta has many extra fields over expected:
257    expected {}
258    received {
259        "location": "uk",
260        "previous_login": "sometime last thursday"
261    }
262
263    extra fields in received:
264        location,
265        previous_login,
266"#
267        );
268    }
269
270    #[test]
271    fn it_should_error_for_nested_contains_via_array_on_differences() {
272        let left = json!({ "name": "John", "comments": [
273            {
274                "text": "Hello",
275                "author": {
276                    "name": "🦊",
277                    "age": 25
278                }
279            },
280            {
281                "text": "Goodbye",
282                "author": {
283                    "name": "John",
284                    "age": 30
285                }
286            }
287        ]});
288
289        let right = json!(expect::object().contains(
290            json!({ "comments": expect::array().contains([
291                json!({
292                    "text": "Hello",
293                    "author": expect::object().contains(
294                        json!({
295                            "name": "Jane",
296                        })
297                    )
298                }),
299            ])})
300        ));
301
302        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
303        assert_eq!(
304            output,
305            r#"Json array at root.comments does not contain expected value:
306    expected array to contain {
307        "author": expect::object(),
308        "text": "Hello"
309    }, but it was not found.
310    received [
311        {
312            "author": {
313                "age": 25,
314                "name": "🦊"
315            },
316            "text": "Hello"
317        },
318        {
319            "author": {
320                "age": 30,
321                "name": "John"
322            },
323            "text": "Goodbye"
324        }
325    ]"#
326        );
327    }
328
329    #[test]
330    fn it_should_error_for_nested_contains_via_object_with_inner_contains_error() {
331        let left = json!({
332            "name": "John",
333            "comment": {
334                "text": "Hello",
335                "author": {
336                    "name": "🦊",
337                    "age": 25
338                }
339            },
340        });
341
342        let right = json!(expect::object().contains(json!({ "comment":
343            expect::object().contains(
344                json!({
345                    "text": "Hello",
346                    "author": expect::object().contains(
347                        json!({
348                            "name": "Jane",
349                        })
350                    )
351                })
352            )
353        })));
354
355        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
356        assert_eq!(
357            output,
358            r#"Json strings at root.comment.author.name are not equal:
359    expected "Jane"
360    received "🦊""#
361        );
362    }
363
364    // TODO, is this correct?
365    // The error message looks like it is checking the key against an expect op.
366    #[test]
367    fn it_should_error_for_nested_contains_via_different_object_with_inner_contains_error() {
368        let left = json!({
369            "name": "John",
370            "comment": {
371                "text": "Hello",
372                "author": {
373                    "name": "Jane",
374                    "age": 25
375                }
376            },
377        });
378
379        let right = json!(expect::object().contains(json!({ "comment":
380            expect::object().contains(
381                json!({
382                    "text": "Hello",
383                    "author": expect::object().contains(
384                        json!({
385                            "something_else": "🦊",
386                        })
387                    )
388                })
389            )
390        })));
391
392        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
393        assert_eq!(
394            output,
395            r#"Json object at root.comment.author is missing key for object:
396    expected field 'something_else',
397    but it was not found"#
398        );
399    }
400}
401
402#[cfg(test)]
403mod test_propagated_contains {
404    use crate::expect;
405    use crate::expect_json_eq;
406    use pretty_assertions::assert_eq;
407    use serde_json::json;
408
409    #[test]
410    fn it_should_be_equal_for_identical_objects() {
411        let left = json!({ "name": "John", "age": 30, "scores": [1, 2, 3] });
412        let right = json!(expect::object()
413            .propagated_contains(json!({ "name": "John", "age": 30, "scores": [1, 2, 3] })));
414
415        let output = expect_json_eq(&left, &right);
416        assert!(output.is_ok());
417    }
418
419    #[test]
420    fn it_should_be_equal_for_reversed_identical_objects() {
421        let left = json!({ "name": "John", "age": 30, "scores": [1, 2, 3] });
422        let right = json!(expect::object()
423            .propagated_contains(json!({ "scores": [1, 2, 3], "age": 30, "name": "John" })));
424
425        let output = expect_json_eq(&left, &right);
426        assert!(output.is_ok());
427    }
428
429    #[test]
430    fn it_should_be_equal_for_partial_contains() {
431        let left = json!({ "name": "John", "age": 30, "scores": [1, 2, 3] });
432        let right =
433            json!(expect::object().propagated_contains(json!({ "name": "John", "age": 30 })));
434
435        let output = expect_json_eq(&left, &right);
436        assert!(output.is_ok());
437    }
438
439    #[test]
440    fn it_should_be_equal_for_nested_contains() {
441        let left = json!({ "name": "John", "comments": [
442            {
443                "text": "Hello",
444                "author": {
445                    "name": "Jane",
446                    "age": 25
447                }
448            },
449            {
450                "text": "Goodbye",
451                "author": {
452                    "name": "John",
453                    "age": 30
454                }
455            }
456        ]});
457
458        let right = json!(expect::object().propagated_contains(
459            json!({ "comments": expect::array().contains([
460                json!({
461                    "text": "Hello",
462                    "author": expect::object().contains(
463                        json!({
464                            "name": "Jane",
465                        })
466                    )
467                }),
468            ])})
469        ));
470
471        let output = expect_json_eq(&left, &right);
472        assert!(output.is_ok(), "{}", output.unwrap_err().to_string());
473    }
474
475    #[test]
476    fn it_should_accept_nested_object_with_missing_fields() {
477        let left = json!({
478            "name": "John",
479            "id": "abd123",
480            "meta": {
481                "location": "uk",
482                "previous_login": "sometime last thursday",
483            }
484        });
485
486        let right = json!(expect::object().propagated_contains(json!({
487            "name": "John",
488            "meta": {},
489        })));
490
491        let result = expect_json_eq(&left, &right);
492        assert!(result.is_ok(), "{result:#?}");
493    }
494
495    #[test]
496    fn it_should_error_if_inner_contains_fails_to_contain_fields() {
497        let left = json!({
498            "name": "John",
499            "id": "abd123",
500            "meta": {
501                "inner": {
502                    "location": "uk",
503                    "previous_login": "sometime last thursday",
504                }
505            }
506        });
507
508        let right = json!(expect::object().propagated_contains(json!({
509            "name": "John",
510            "meta": expect::object().contains(json!({
511                "inner": {}
512            })),
513        })));
514
515        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
516        assert_eq!(
517            output,
518            r#"Json object at root.meta.inner has many extra fields over expected:
519    expected {}
520    received {
521        "location": "uk",
522        "previous_login": "sometime last thursday"
523    }
524
525    extra fields in received:
526        location,
527        previous_login,
528"#
529        );
530    }
531
532    #[test]
533    fn it_should_error_for_same_fields_but_different_values() {
534        let left = json!({ "name": "John", "age": 30, "scores": [1, 2, 3] });
535        let right = json!(expect::object()
536            .propagated_contains(json!({ "name": "Joe", "age": 31, "scores": [4, 5, 6] })));
537
538        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
539        assert_eq!(
540            output,
541            r#"Json integers at root.age are not equal:
542    expected 31
543    received 30"#
544        );
545    }
546
547    #[test]
548    fn it_should_be_ok_for_empty_contains() {
549        let left = json!({ "name": "John", "age": 30, "scores": [1, 2, 3] });
550        let right = json!(expect::object().propagated_contains(json!({})));
551
552        let output = expect_json_eq(&left, &right);
553        assert!(output.is_ok());
554    }
555
556    #[test]
557    fn it_should_be_ok_for_empty_on_empty_object() {
558        let left = json!({});
559        let right = json!(expect::object().propagated_contains(json!({})));
560
561        let output = expect_json_eq(&left, &right);
562        assert!(output.is_ok());
563    }
564
565    #[test]
566    fn it_should_error_if_used_against_the_wrong_type() {
567        let left = json!("🦊");
568        let right = json!(expect::object().propagated_contains(json!({})));
569
570        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
571        assert_eq!(
572            output,
573            r#"Json expect::object() at root, received wrong type:
574    expected object
575    received string "🦊""#
576        );
577    }
578
579    #[test]
580    fn it_should_error_for_nested_contains_via_object_with_inner_contains_error() {
581        let left = json!({
582            "name": "John",
583            "comment": {
584                "text": "Hello",
585                "author": {
586                    "name": "🦊",
587                    "age": 25
588                }
589            },
590        });
591
592        let right = json!(expect::object().propagated_contains(json!({ "comment":
593            expect::object().propagated_contains(
594                json!({
595                    "text": "Hello",
596                    "author": expect::object().contains(
597                        json!({
598                            "name": "Jane",
599                        })
600                    )
601                })
602            )
603        })));
604
605        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
606        assert_eq!(
607            output,
608            r#"Json strings at root.comment.author.name are not equal:
609    expected "Jane"
610    received "🦊""#
611        );
612    }
613
614    // TODO, is this correct?
615    // The error message looks like it is checking the key against an expect op.
616    #[test]
617    fn it_should_error_for_nested_contains_via_different_object_with_inner_contains_error() {
618        let left = json!({
619            "name": "John",
620            "comment": {
621                "text": "Hello",
622                "author": {
623                    "name": "Jane",
624                    "age": 25
625                }
626            },
627        });
628
629        let right = json!(expect::object().propagated_contains(json!({ "comment":
630            expect::object().propagated_contains(
631                json!({
632                    "text": "Hello",
633                    "author": expect::object().contains(
634                        json!({
635                            "something_else": "🦊",
636                        })
637                    )
638                })
639            )
640        })));
641
642        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
643        assert_eq!(
644            output,
645            r#"Json object at root.comment.author is missing key for object:
646    expected field 'something_else',
647    but it was not found"#
648        );
649    }
650}
651
652#[cfg(test)]
653mod test_empty {
654    use crate::expect;
655    use crate::expect_json_eq;
656    use pretty_assertions::assert_eq;
657    use serde_json::json;
658
659    #[test]
660    fn it_should_pass_when_object_is_empty() {
661        let left = json!({});
662        let right = json!(expect::object().empty());
663
664        let output = expect_json_eq(&left, &right);
665        assert!(output.is_ok(), "assertion error: {output:#?}");
666    }
667
668    #[test]
669    fn it_should_fail_when_object_is_not_empty() {
670        let left = json!({ "foo": "bar" });
671        let right = json!(expect::object().empty());
672
673        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
674        assert_eq!(
675            output,
676            r#"Json expect::object() error at root:
677    expected empty object
678    received {
679        "foo": "bar"
680    }"#
681        );
682    }
683}
684
685#[cfg(test)]
686mod test_not_empty {
687    use crate::expect;
688    use crate::expect_json_eq;
689    use pretty_assertions::assert_eq;
690    use serde_json::json;
691
692    #[test]
693    fn it_should_pass_when_object_is_not_empty() {
694        let left = json!({ "foo": "bar" });
695        let right = json!(expect::object().not_empty());
696
697        let output = expect_json_eq(&left, &right);
698        assert!(output.is_ok(), "assertion error: {output:#?}");
699    }
700
701    #[test]
702    fn it_should_fail_when_object_is_empty() {
703        let left = json!({});
704        let right = json!(expect::object().not_empty());
705
706        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
707        assert_eq!(
708            output,
709            r#"Json expect::object() error at root:
710    expected non-empty object
711    received {}"#
712        );
713    }
714}