//! Auth workflow: signup -> dashboard -> logout -> dashboard (redirect) use crate::harness::TestHarness; #[tokio::test] async fn signup_login_logout_flow() { let mut h = TestHarness::new().await; // Sign up let _user_id = h.signup("testuser", "test@example.com", "password123").await; // Should be logged in — dashboard returns 200 let resp = h.client.get("/dashboard").await; assert_eq!(resp.status, 200, "Dashboard should be accessible after signup"); // Logout let resp = h.client.post_form("/logout", "").await; assert!( resp.status.is_success() || resp.status.is_redirection(), "Logout should succeed" ); // Dashboard should now redirect (302) or return 401 let resp = h.client.get("/dashboard").await; assert!( resp.status == 302 || resp.status == 303 || resp.status == 401, "Dashboard should redirect after logout, got {}", resp.status ); } #[tokio::test] async fn login_with_existing_account() { let mut h = TestHarness::new().await; // Sign up and then log out let _user_id = h.signup("alice", "alice@example.com", "secure_pass99").await; h.client.post_form("/logout", "").await; // Log back in h.login("alice", "secure_pass99").await; // Dashboard should be accessible let resp = h.client.get("/dashboard").await; assert_eq!(resp.status, 200, "Dashboard should be accessible after login"); } #[tokio::test] async fn wrong_password_rejected() { let mut h = TestHarness::new().await; let _user_id = h.signup("wp_user", "wp@example.com", "correctpass1").await; h.client.post_form("/logout", "").await; let resp = h .client .post_form("/login", "login=wp_user&password=totallyWrong") .await; assert!( resp.status != 200 && resp.status != 303, "Wrong password should not yield 200 or 303, got {}", resp.status ); } #[tokio::test] async fn nonexistent_user_rejected() { let mut h = TestHarness::new().await; let resp = h .client .post_form("/login", "login=ghost_user_xyz&password=anypass123") .await; assert!( resp.status != 200 && resp.status != 303, "Nonexistent user login should not yield 200 or 303, got {}", resp.status ); } #[tokio::test] async fn duplicate_email_rejected() { let mut h = TestHarness::new().await; let _user_id = h.signup("orig_user", "dupe@example.com", "password123").await; h.client.post_form("/logout", "").await; // Attempt signup with the same email but a different username let resp = h .client .post_form( "/join", "username=other_user&email=dupe@example.com&password=password123&password_confirm=password123", ) .await; assert!( resp.status.is_client_error() || resp.text.contains("already") || resp.text.contains("taken"), "Duplicate email signup should fail: {} {}", resp.status, resp.text ); } #[tokio::test] async fn duplicate_username_rejected() { let mut h = TestHarness::new().await; let _user_id = h.signup("taken_name", "first@example.com", "password123").await; h.client.post_form("/logout", "").await; // Attempt signup with the same username but a different email let resp = h .client .post_form( "/join", "username=taken_name&email=second@example.com&password=password123&password_confirm=password123", ) .await; assert!( resp.status.is_client_error() || resp.text.contains("already") || resp.text.contains("taken"), "Duplicate username signup should fail: {} {}", resp.status, resp.text ); } #[tokio::test] async fn login_with_email() { let mut h = TestHarness::new().await; let _user_id = h.signup("emaillogin", "emaillogin@example.com", "password123").await; h.client.post_form("/logout", "").await; // Login using email address instead of username h.login("emaillogin@example.com", "password123").await; let resp = h.client.get("/dashboard").await; assert_eq!( resp.status, 200, "Dashboard should be accessible after login with email" ); } #[tokio::test] async fn password_change_flow() { let mut h = TestHarness::new().await; let _user_id = h.signup("pwchange", "pwchange@example.com", "oldpass123").await; // Change password via PUT form let resp = h .client .put_form( "/api/users/me/password", "current_password=oldpass123&new_password=newpass456", ) .await; assert!( resp.status.is_success(), "Password change should succeed: {} {}", resp.status, resp.text ); // Logout h.client.post_form("/logout", "").await; // Login with new password should succeed h.login("pwchange", "newpass456").await; let resp = h.client.get("/dashboard").await; assert_eq!(resp.status, 200, "New password should grant dashboard access"); // Logout and try old password h.client.post_form("/logout", "").await; let resp = h .client .post_form("/login", "login=pwchange&password=oldpass123") .await; assert!( resp.status != 200 && resp.status != 303, "Old password should no longer work, got {}", resp.status ); } #[tokio::test] async fn lockout_after_failed_attempts() { let mut h = TestHarness::new().await; let _user_id = h.signup("lockme", "lockme@example.com", "rightpass1").await; h.client.post_form("/logout", "").await; // 5 failed login attempts. `/login` is Manual-CSRF now (Phase 2), so // each attempt must refresh the token — the helper handles that. for _ in 0..5 { h.failed_login_attempt("lockme", "wrongwrong").await; } // Now try with correct password -- should be locked out. login_handler // returns 200 with the form re-rendered + inline "Account is locked" // message (same UX convention as the inline-error pattern); a successful // login would be a 303 redirect, so absence of redirect + body containing // "locked" together prove lockout. let resp = h.failed_login_attempt("lockme", "rightpass1").await; assert!( !resp.status.is_redirection(), "Lockout should not redirect to dashboard, got {}", resp.status ); assert!( resp.text.to_lowercase().contains("locked"), "Response should mention lockout: {}", resp.text ); }