/ tests / requests / auth.rs
auth.rs
  1  use microvisor::{app::App, models::users};
  2  use insta::{assert_debug_snapshot, with_settings};
  3  use loco_rs::testing::prelude::*;
  4  use rstest::rstest;
  5  use serial_test::serial;
  6  
  7  use super::prepare_data;
  8  
  9  // TODO: see how to dedup / extract this to app-local test utils
 10  // not to framework, because that would require a runtime dep on insta
 11  macro_rules! configure_insta {
 12      ($($expr:expr),*) => {
 13          let mut settings = insta::Settings::clone_current();
 14          settings.set_prepend_module_to_snapshot(false);
 15          settings.set_snapshot_suffix("auth_request");
 16          let _guard = settings.bind_to_scope();
 17      };
 18  }
 19  
 20  #[tokio::test]
 21  #[serial]
 22  async fn can_register() {
 23      configure_insta!();
 24  
 25      request::<App, _, _>(|request, ctx| async move {
 26          let email = "test@loco.com";
 27          let payload = serde_json::json!({
 28              "name": "loco",
 29              "email": email,
 30              "password": "12341234"
 31          });
 32  
 33          let response = request.post("/api/auth/register").json(&payload).await;
 34          assert_eq!(
 35              response.status_code(),
 36              200,
 37              "Register request should succeed"
 38          );
 39          let saved_user = users::Model::find_by_email(&ctx.db, email).await;
 40  
 41          with_settings!({
 42              filters => cleanup_user_model()
 43          }, {
 44              assert_debug_snapshot!(saved_user);
 45          });
 46  
 47          let deliveries = ctx.mailer.unwrap().deliveries();
 48          assert_eq!(deliveries.count, 1, "Exactly one email should be sent");
 49  
 50          // with_settings!({
 51          //     filters => cleanup_email()
 52          // }, {
 53          //     assert_debug_snapshot!(ctx.mailer.unwrap().deliveries());
 54          // });
 55      })
 56      .await;
 57  }
 58  
 59  #[rstest]
 60  #[case("login_with_valid_password", "12341234")]
 61  #[case("login_with_invalid_password", "invalid-password")]
 62  #[tokio::test]
 63  #[serial]
 64  async fn can_login_with_verify(#[case] test_name: &str, #[case] password: &str) {
 65      configure_insta!();
 66  
 67      request::<App, _, _>(|request, ctx| async move {
 68          let email = "test@loco.com";
 69          let register_payload = serde_json::json!({
 70              "name": "loco",
 71              "email": email,
 72              "password": "12341234"
 73          });
 74  
 75          //Creating a new user
 76          let register_response = request
 77              .post("/api/auth/register")
 78              .json(&register_payload)
 79              .await;
 80  
 81          assert_eq!(
 82              register_response.status_code(),
 83              200,
 84              "Register request should succeed"
 85          );
 86  
 87          let user = users::Model::find_by_email(&ctx.db, email).await.unwrap();
 88          let email_verification_token = user
 89              .email_verification_token
 90              .expect("Email verification token should be generated");
 91          request
 92              .get(&format!("/api/auth/verify/{email_verification_token}"))
 93              .await;
 94  
 95          //verify user request
 96          let response = request
 97              .post("/api/auth/login")
 98              .json(&serde_json::json!({
 99                  "email": email,
100                  "password": password
101              }))
102              .await;
103  
104          // Make sure email_verified_at is set
105          let user = users::Model::find_by_email(&ctx.db, email)
106              .await
107              .expect("Failed to find user by email");
108  
109          assert!(
110              user.email_verified_at.is_some(),
111              "Expected the email to be verified, but it was not. User: {:?}",
112              user
113          );
114  
115          with_settings!({
116              filters => cleanup_user_model()
117          }, {
118              assert_debug_snapshot!(test_name, (response.status_code(), response.text()));
119          });
120      })
121      .await;
122  }
123  
124  #[tokio::test]
125  #[serial]
126  async fn can_login_without_verify() {
127      configure_insta!();
128  
129      request::<App, _, _>(|request, _ctx| async move {
130          let email = "test@loco.com";
131          let password = "12341234";
132          let register_payload = serde_json::json!({
133              "name": "loco",
134              "email": email,
135              "password": password
136          });
137  
138          //Creating a new user
139          let register_response = request
140              .post("/api/auth/register")
141              .json(&register_payload)
142              .await;
143  
144          assert_eq!(
145              register_response.status_code(),
146              200,
147              "Register request should succeed"
148          );
149  
150          //verify user request
151          let login_response = request
152              .post("/api/auth/login")
153              .json(&serde_json::json!({
154                  "email": email,
155                  "password": password
156              }))
157              .await;
158  
159          assert_eq!(
160              login_response.status_code(),
161              200,
162              "Login request should succeed"
163          );
164  
165          with_settings!({
166              filters => cleanup_user_model()
167          }, {
168              assert_debug_snapshot!(login_response.text());
169          });
170      })
171      .await;
172  }
173  
174  #[tokio::test]
175  #[serial]
176  async fn can_reset_password() {
177      configure_insta!();
178  
179      request::<App, _, _>(|request, ctx| async move {
180          let login_data = prepare_data::init_user_login(&request, &ctx).await;
181  
182          let forgot_payload = serde_json::json!({
183              "email": login_data.user.email,
184          });
185          let forget_response = request.post("/api/auth/forgot").json(&forgot_payload).await;
186          assert_eq!(
187              forget_response.status_code(),
188              200,
189              "Forget request should succeed"
190          );
191  
192          let user = users::Model::find_by_email(&ctx.db, &login_data.user.email)
193              .await
194              .expect("Failed to find user by email");
195  
196          assert!(
197              user.reset_token.is_some(),
198              "Expected reset_token to be set, but it was None. User: {user:?}"
199          );
200          assert!(
201              user.reset_sent_at.is_some(),
202              "Expected reset_sent_at to be set, but it was None. User: {user:?}"
203          );
204  
205          let new_password = "new-password";
206          let reset_payload = serde_json::json!({
207              "token": user.reset_token,
208              "password": new_password,
209          });
210  
211          let reset_response = request.post("/api/auth/reset").json(&reset_payload).await;
212          assert_eq!(
213              reset_response.status_code(),
214              200,
215              "Reset password request should succeed"
216          );
217  
218          let user = users::Model::find_by_email(&ctx.db, &user.email)
219              .await
220              .unwrap();
221  
222          assert!(user.reset_token.is_none());
223          assert!(user.reset_sent_at.is_none());
224  
225          assert_debug_snapshot!(reset_response.text());
226  
227          let login_response = request
228              .post("/api/auth/login")
229              .json(&serde_json::json!({
230                  "email": user.email,
231                  "password": new_password
232              }))
233              .await;
234  
235          assert_eq!(
236              login_response.status_code(),
237              200,
238              "Login request should succeed"
239          );
240  
241          let deliveries = ctx.mailer.unwrap().deliveries();
242          assert_eq!(deliveries.count, 2, "Exactly one email should be sent");
243          // with_settings!({
244          //     filters => cleanup_email()
245          // }, {
246          //     assert_debug_snapshot!(deliveries.messages);
247          // });
248      })
249      .await;
250  }
251  
252  #[tokio::test]
253  #[serial]
254  async fn can_get_current_user() {
255      configure_insta!();
256  
257      request::<App, _, _>(|request, ctx| async move {
258          let user = prepare_data::init_user_login(&request, &ctx).await;
259  
260          let (auth_key, auth_value) = prepare_data::auth_header(&user.token);
261          let response = request
262              .get("/api/auth/current")
263              .add_header(auth_key, auth_value)
264              .await;
265  
266          assert_eq!(
267              response.status_code(),
268              200,
269              "Current request should succeed"
270          );
271  
272          with_settings!({
273              filters => cleanup_user_model()
274          }, {
275              assert_debug_snapshot!((response.status_code(), response.text()));
276          });
277      })
278      .await;
279  }
280  
281  #[tokio::test]
282  #[serial]
283  async fn can_auth_with_magic_link() {
284      configure_insta!();
285      request::<App, _, _>(|request, ctx| async move {
286          seed::<App>(&ctx).await.unwrap();
287  
288          let payload = serde_json::json!({
289              "email": "user1@example.com",
290          });
291          let response = request.post("/api/auth/magic-link").json(&payload).await;
292          assert_eq!(
293              response.status_code(),
294              200,
295              "Magic link request should succeed"
296          );
297  
298          let deliveries = ctx.mailer.unwrap().deliveries();
299          assert_eq!(deliveries.count, 1, "Exactly one email should be sent");
300  
301          // let redact_token = format!("[a-zA-Z0-9]{{{}}}", users::MAGIC_LINK_LENGTH);
302          // with_settings!({
303          //      filters => {
304          //          let mut combined_filters = cleanup_email().clone();
305          //         combined_filters.extend(vec![(r"(\\r\\n|=\\r\\n)", ""), (redact_token.as_str(), "[REDACT_TOKEN]") ]);
306          //         combined_filters
307          //     }
308          // }, {
309          //     assert_debug_snapshot!(deliveries.messages);
310          // });
311  
312          let user = users::Model::find_by_email(&ctx.db, "user1@example.com")
313              .await
314              .expect("User should be found");
315  
316          let magic_link_token = user
317              .magic_link_token
318              .expect("Magic link token should be generated");
319          let magic_link_response = request
320              .get(&format!("/api/auth/magic-link/{magic_link_token}"))
321              .await;
322          assert_eq!(
323              magic_link_response.status_code(),
324              200,
325              "Magic link authentication should succeed"
326          );
327  
328          with_settings!({
329              filters => cleanup_user_model()
330          }, {
331              assert_debug_snapshot!(magic_link_response.text());
332          });
333      })
334      .await;
335  }
336  
337  #[tokio::test]
338  #[serial]
339  async fn can_reject_invalid_email() {
340      configure_insta!();
341      request::<App, _, _>(|request, _ctx| async move {
342          let invalid_email = "user1@temp-mail.com";
343          let payload = serde_json::json!({
344              "email": invalid_email,
345          });
346          let response = request.post("/api/auth/magic-link").json(&payload).await;
347          assert_eq!(
348              response.status_code(),
349              400,
350              "Expected request with invalid email '{invalid_email}' to be blocked, but it was allowed."
351          );
352      })
353      .await;
354  }
355  
356  #[tokio::test]
357  #[serial]
358  async fn can_reject_invalid_magic_link_token() {
359      configure_insta!();
360      request::<App, _, _>(|request, ctx| async move {
361          seed::<App>(&ctx).await.unwrap();
362  
363          let magic_link_response = request.get("/api/auth/magic-link/invalid-token").await;
364          assert_eq!(
365              magic_link_response.status_code(),
366              401,
367              "Magic link authentication should be rejected"
368          );
369      })
370      .await;
371  }