expect_json/expects/ops/
expect_email.rs

1use crate::expect_op;
2use crate::Context;
3use crate::ExpectOp;
4use crate::ExpectOpError;
5use crate::ExpectOpResult;
6use crate::JsonType;
7use core::str::FromStr;
8use email_address::EmailAddress;
9
10///
11/// Expects a valid email address string.
12///
13/// You can build these using the [`crate::expect::email`] function.
14///
15#[expect_op(internal, name = "email")]
16#[derive(Debug, Clone, Default, PartialEq)]
17pub struct ExpectEmail {
18    expected_domain: Option<String>,
19    expected_local_part: Option<String>,
20}
21
22impl ExpectEmail {
23    pub(crate) fn new() -> Self {
24        Self {
25            expected_domain: None,
26            expected_local_part: None,
27        }
28    }
29
30    ///
31    /// Expects the local part of the email address.
32    /// The 'local part' is the part before the '@' symbol.
33    /// i.e. the 'joe' in 'joe@example.com'.
34    ///
35    /// ```rust
36    /// # async fn test() -> Result<(), Box<dyn ::std::error::Error>> {
37    /// #
38    /// # use axum::Router;
39    /// # use axum::extract::Json;
40    /// # use axum::routing::get;
41    /// # use axum_test::TestServer;
42    /// # use serde_json::json;
43    /// #
44    /// # let server = TestServer::new(Router::new())?;
45    /// #
46    /// use axum_test::expect_json::expect;
47    ///
48    /// let server = TestServer::new(Router::new())?;
49    ///
50    /// server.get(&"/user")
51    ///     .await
52    ///     .assert_json(&json!({
53    ///         "name": "Joe",
54    ///         "email": expect::email().local_part("joe"),
55    ///     }));
56    /// #
57    /// # Ok(()) }
58    /// ```
59    ///
60    pub fn local_part<S>(mut self, local_part: S) -> Self
61    where
62        S: Into<String>,
63    {
64        self.expected_local_part = Some(local_part.into());
65        self
66    }
67
68    ///
69    /// Expects the domain part of the email address.
70    /// i.e. 'example.com' in 'joe@example.com'.
71    ///
72    /// ```rust
73    /// # async fn test() -> Result<(), Box<dyn ::std::error::Error>> {
74    /// #
75    /// # use axum::Router;
76    /// # use axum::extract::Json;
77    /// # use axum::routing::get;
78    /// # use axum_test::TestServer;
79    /// # use serde_json::json;
80    /// #
81    /// # let server = TestServer::new(Router::new())?;
82    /// #
83    /// use axum_test::expect_json::expect;
84    ///
85    /// let server = TestServer::new(Router::new())?;
86    ///
87    /// server.get(&"/user")
88    ///     .await
89    ///     .assert_json(&json!({
90    ///         "name": "Joe",
91    ///         "email": expect::email().domain("example.com"),
92    ///     }));
93    /// #
94    /// # Ok(()) }
95    /// ```
96    ///
97    pub fn domain<S>(mut self, domain: S) -> Self
98    where
99        S: Into<String>,
100    {
101        self.expected_domain = Some(domain.into());
102        self
103    }
104}
105
106impl ExpectOp for ExpectEmail {
107    fn on_string(&self, context: &mut Context, received: &str) -> ExpectOpResult<()> {
108        let email = EmailAddress::from_str(received).map_err(|e| {
109            let error_message = format!("Invalid email address, received '{received}'");
110            ExpectOpError::custom_error(context, self, error_message, e)
111        })?;
112
113        if let Some(expected_local_part) = &self.expected_local_part {
114            if email.local_part() != expected_local_part {
115                return Err(ExpectOpError::custom(
116                    context,
117                    self,
118                    format!(
119                        "Local part mismatch, expected '{}', received '{received}'",
120                        expected_local_part
121                    ),
122                ));
123            }
124        }
125
126        if let Some(expected_domain) = &self.expected_domain {
127            if email.domain() != expected_domain {
128                return Err(ExpectOpError::custom(
129                    context,
130                    self,
131                    format!(
132                        "Domain mismatch, expected '{}', received '{received}'",
133                        expected_domain
134                    ),
135                ));
136            }
137        }
138
139        Ok(())
140    }
141
142    fn supported_types(&self) -> &'static [JsonType] {
143        &[JsonType::String]
144    }
145}
146
147#[cfg(test)]
148mod test_email {
149    use crate::expect;
150    use crate::expect_json_eq;
151    use pretty_assertions::assert_eq;
152    use serde_json::json;
153
154    #[test]
155    fn it_should_accept_valid_email() {
156        let left = json!("test@example.com");
157        let right = json!(expect::email());
158
159        let output = expect_json_eq(&left, &right);
160        assert!(output.is_ok());
161    }
162
163    #[test]
164    fn it_should_accept_valid_email_with_plus_sign() {
165        let left = json!("test+test@example.com");
166        let right = json!(expect::email());
167
168        let output = expect_json_eq(&left, &right);
169        assert!(output.is_ok());
170    }
171
172    #[test]
173    fn it_should_accept_email_inside_object() {
174        let left = json!({ "name": "Joe", "email": "test@example.com" });
175        let right = json!({
176            "name": "Joe",
177            "email": expect::email(),
178        });
179
180        let output = expect_json_eq(&left, &right);
181        assert!(output.is_ok());
182    }
183
184    #[test]
185    fn it_should_reject_invalid_email() {
186        let left = json!("🦊");
187        let right = json!(expect::email());
188
189        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
190        assert_eq!(
191            output,
192            format!(
193                r#"Json expect::email() error at root:
194    Invalid email address, received '🦊',
195    Missing separator character '@'."#
196            )
197        );
198    }
199}
200
201#[cfg(test)]
202mod test_local_part {
203    use crate::expect;
204    use crate::expect_json_eq;
205    use pretty_assertions::assert_eq;
206    use serde_json::json;
207
208    #[test]
209    fn it_should_accept_valid_local_part() {
210        let left = json!("test@example.com");
211        let right = json!(expect::email().local_part("test"));
212
213        let output = expect_json_eq(&left, &right);
214        assert!(output.is_ok());
215    }
216
217    #[test]
218    fn it_should_reject_invalid_local_part() {
219        let left = json!("test@example.com");
220        let right = json!(expect::email().local_part("🦊"));
221
222        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
223        assert_eq!(
224            output,
225            format!(
226                r#"Json expect::email() error at root:
227    Local part mismatch, expected '🦊', received 'test@example.com'"#
228            )
229        );
230    }
231}
232
233#[cfg(test)]
234mod test_domain {
235    use crate::expect;
236    use crate::expect_json_eq;
237    use pretty_assertions::assert_eq;
238    use serde_json::json;
239
240    #[test]
241    fn it_should_accept_valid_domain() {
242        let left = json!("test@example.com");
243        let right = json!(expect::email().domain("example.com"));
244
245        let output = expect_json_eq(&left, &right);
246        assert!(output.is_ok());
247    }
248
249    #[test]
250    fn it_should_reject_invalid_domain() {
251        let left = json!("test@example.com");
252        let right = json!(expect::email().domain("🦊.fox"));
253
254        let output = expect_json_eq(&left, &right).unwrap_err().to_string();
255        assert_eq!(
256            output,
257            format!(
258                r#"Json expect::email() error at root:
259    Domain mismatch, expected '🦊.fox', received 'test@example.com'"#
260            )
261        );
262    }
263}