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(®ister_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(®ister_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 }