use insta::{assert_debug_snapshot, with_settings}; use loco_rs::testing::prelude::*; use music_metadata_manager::{app::App, models::users}; use rstest::rstest; use serial_test::serial; use super::prepare_data; // TODO: see how to dedup / extract this to app-local test utils // not to framework, because that would require a runtime dep on insta macro_rules! configure_insta { ($($expr:expr),*) => { let mut settings = insta::Settings::clone_current(); settings.set_prepend_module_to_snapshot(false); settings.set_snapshot_suffix("auth_request"); let _guard = settings.bind_to_scope(); }; } #[tokio::test] #[serial] async fn can_register() { configure_insta!(); request::(|request, ctx| async move { let email = "test@loco.com"; let payload = serde_json::json!({ "name": "loco", "email": email, "password": "12341234" }); let response = request.post("/api/auth/register").json(&payload).await; assert_eq!( response.status_code(), 200, "Register request should succeed" ); let saved_user = users::Model::find_by_email(&ctx.db, email).await; with_settings!({ filters => cleanup_user_model() }, { assert_debug_snapshot!(saved_user); }); let deliveries = ctx.mailer.unwrap().deliveries(); assert_eq!(deliveries.count, 1, "Exactly one email should be sent"); // with_settings!({ // filters => cleanup_email() // }, { // assert_debug_snapshot!(ctx.mailer.unwrap().deliveries()); // }); }) .await; } #[rstest] #[case("login_with_valid_password", "12341234")] #[case("login_with_invalid_password", "invalid-password")] #[tokio::test] #[serial] async fn can_login_with_verify(#[case] test_name: &str, #[case] password: &str) { configure_insta!(); request::(|request, ctx| async move { let email = "test@loco.com"; let register_payload = serde_json::json!({ "name": "loco", "email": email, "password": "12341234" }); //Creating a new user let register_response = request .post("/api/auth/register") .json(®ister_payload) .await; assert_eq!( register_response.status_code(), 200, "Register request should succeed" ); let user = users::Model::find_by_email(&ctx.db, email).await.unwrap(); let email_verification_token = user .email_verification_token .expect("Email verification token should be generated"); request .get(&format!("/api/auth/verify/{email_verification_token}")) .await; //verify user request let response = request .post("/api/auth/login") .json(&serde_json::json!({ "email": email, "password": password })) .await; // Make sure email_verified_at is set let user = users::Model::find_by_email(&ctx.db, email) .await .expect("Failed to find user by email"); assert!( user.email_verified_at.is_some(), "Expected the email to be verified, but it was not. User: {:?}", user ); with_settings!({ filters => cleanup_user_model() }, { assert_debug_snapshot!(test_name, (response.status_code(), response.text())); }); }) .await; } #[tokio::test] #[serial] async fn can_login_without_verify() { configure_insta!(); request::(|request, _ctx| async move { let email = "test@loco.com"; let password = "12341234"; let register_payload = serde_json::json!({ "name": "loco", "email": email, "password": password }); //Creating a new user let register_response = request .post("/api/auth/register") .json(®ister_payload) .await; assert_eq!( register_response.status_code(), 200, "Register request should succeed" ); //verify user request let login_response = request .post("/api/auth/login") .json(&serde_json::json!({ "email": email, "password": password })) .await; assert_eq!( login_response.status_code(), 200, "Login request should succeed" ); with_settings!({ filters => cleanup_user_model() }, { assert_debug_snapshot!(login_response.text()); }); }) .await; } #[tokio::test] #[serial] async fn can_reset_password() { configure_insta!(); request::(|request, ctx| async move { let login_data = prepare_data::init_user_login(&request, &ctx).await; let forgot_payload = serde_json::json!({ "email": login_data.user.email, }); let forget_response = request.post("/api/auth/forgot").json(&forgot_payload).await; assert_eq!( forget_response.status_code(), 200, "Forget request should succeed" ); let user = users::Model::find_by_email(&ctx.db, &login_data.user.email) .await .expect("Failed to find user by email"); assert!( user.reset_token.is_some(), "Expected reset_token to be set, but it was None. User: {user:?}" ); assert!( user.reset_sent_at.is_some(), "Expected reset_sent_at to be set, but it was None. User: {user:?}" ); let new_password = "new-password"; let reset_payload = serde_json::json!({ "token": user.reset_token, "password": new_password, }); let reset_response = request.post("/api/auth/reset").json(&reset_payload).await; assert_eq!( reset_response.status_code(), 200, "Reset password request should succeed" ); let user = users::Model::find_by_email(&ctx.db, &user.email) .await .unwrap(); assert!(user.reset_token.is_none()); assert!(user.reset_sent_at.is_none()); assert_debug_snapshot!(reset_response.text()); let login_response = request .post("/api/auth/login") .json(&serde_json::json!({ "email": user.email, "password": new_password })) .await; assert_eq!( login_response.status_code(), 200, "Login request should succeed" ); let deliveries = ctx.mailer.unwrap().deliveries(); assert_eq!(deliveries.count, 2, "Exactly one email should be sent"); // with_settings!({ // filters => cleanup_email() // }, { // assert_debug_snapshot!(deliveries.messages); // }); }) .await; } #[tokio::test] #[serial] async fn can_get_current_user() { configure_insta!(); request::(|request, ctx| async move { let user = prepare_data::init_user_login(&request, &ctx).await; let (auth_key, auth_value) = prepare_data::auth_header(&user.token); let response = request .get("/api/auth/current") .add_header(auth_key, auth_value) .await; assert_eq!( response.status_code(), 200, "Current request should succeed" ); with_settings!({ filters => cleanup_user_model() }, { assert_debug_snapshot!((response.status_code(), response.text())); }); }) .await; } #[tokio::test] #[serial] async fn can_auth_with_magic_link() { configure_insta!(); request::(|request, ctx| async move { seed::(&ctx).await.unwrap(); let payload = serde_json::json!({ "email": "user1@example.com", }); let response = request.post("/api/auth/magic-link").json(&payload).await; assert_eq!( response.status_code(), 200, "Magic link request should succeed" ); let deliveries = ctx.mailer.unwrap().deliveries(); assert_eq!(deliveries.count, 1, "Exactly one email should be sent"); // let redact_token = format!("[a-zA-Z0-9]{{{}}}", users::MAGIC_LINK_LENGTH); // with_settings!({ // filters => { // let mut combined_filters = cleanup_email().clone(); // combined_filters.extend(vec![(r"(\\r\\n|=\\r\\n)", ""), (redact_token.as_str(), "[REDACT_TOKEN]") ]); // combined_filters // } // }, { // assert_debug_snapshot!(deliveries.messages); // }); let user = users::Model::find_by_email(&ctx.db, "user1@example.com") .await .expect("User should be found"); let magic_link_token = user .magic_link_token .expect("Magic link token should be generated"); let magic_link_response = request .get(&format!("/api/auth/magic-link/{magic_link_token}")) .await; assert_eq!( magic_link_response.status_code(), 200, "Magic link authentication should succeed" ); with_settings!({ filters => cleanup_user_model() }, { assert_debug_snapshot!(magic_link_response.text()); }); }) .await; } #[tokio::test] #[serial] async fn can_reject_invalid_email() { configure_insta!(); request::(|request, _ctx| async move { let invalid_email = "user1@temp-mail.com"; let payload = serde_json::json!({ "email": invalid_email, }); let response = request.post("/api/auth/magic-link").json(&payload).await; assert_eq!( response.status_code(), 400, "Expected request with invalid email '{invalid_email}' to be blocked, but it was allowed." ); }) .await; } #[tokio::test] #[serial] async fn can_reject_invalid_magic_link_token() { configure_insta!(); request::(|request, ctx| async move { seed::(&ctx).await.unwrap(); let magic_link_response = request.get("/api/auth/magic-link/invalid-token").await; assert_eq!( magic_link_response.status_code(), 401, "Magic link authentication should be rejected" ); }) .await; }