expect_json/expect/ops/expect_object/
expect_object.rs

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