//! Account management: profile updates, password changes, email verification, login links. use crate::harness::TestHarness; use serde_json::Value; const SIGNING_SECRET: &str = "test-signing-secret-for-integration-tests"; #[tokio::test] async fn update_profile() { let mut h = TestHarness::new().await; let _user_id = h.signup("profuser", "profuser@test.com", "password123").await; // Update profile via form (route uses Form extractor) let resp = h .client .put_form( "/api/users/me", "display_name=New+Name&bio=Hello+world", ) .await; assert!( resp.status.is_success(), "Update profile failed: {} {}", resp.status, resp.text ); // Non-HTMX form request returns JSON ProfileResponse let profile: Value = resp.json(); assert_eq!(profile["username"].as_str().unwrap(), "profuser"); assert_eq!(profile["display_name"].as_str().unwrap(), "New Name"); assert_eq!(profile["bio"].as_str().unwrap(), "Hello world"); } #[tokio::test] async fn change_password() { let mut h = TestHarness::new().await; let _user_id = h .signup("passuser", "passuser@test.com", "oldpassword1") .await; // Change password let resp = h .client .put_form( "/api/users/me/password", "current_password=oldpassword1&new_password=newpassword1", ) .await; assert!( resp.status.is_success(), "Change password failed: {} {}", resp.status, resp.text ); // Logout h.client.post_form("/logout", "").await; // Login with new password should work h.login("passuser", "newpassword1").await; let resp = h.client.get("/dashboard").await; assert_eq!(resp.status, 200, "Should access dashboard with new password"); } #[tokio::test] async fn change_password_wrong_current() { let mut h = TestHarness::new().await; let _user_id = h .signup("badpass", "badpass@test.com", "password123") .await; // Try to change with wrong current password let resp = h .client .put_form( "/api/users/me/password", "current_password=wrongpassword&new_password=newpassword1", ) .await; assert_eq!( resp.status, 400, "Should reject wrong current password, got {} {}", resp.status, resp.text ); } #[tokio::test] async fn email_verification_via_signed_link() { let mut h = TestHarness::new().await; let user_id = h .signup("verifyuser", "verify@test.com", "password123") .await; // Ensure email_verified is false so the verification link works sqlx::query("UPDATE users SET email_verified = false WHERE id = $1") .bind(user_id) .execute(&h.db) .await .unwrap(); // Generate a verification URL the same way the app does let url = makenotwork::email::generate_verification_url("", user_id, "verify@test.com", SIGNING_SECRET); // URL is like "/verify-email?user=...&expires=...&sig=..." let path = url.strip_prefix("").unwrap_or(&url); let resp = h.client.get(path).await; assert!( resp.status.is_success(), "Verify email failed: {} {}", resp.status, resp.text ); assert!( resp.text.contains("Email Verified"), "Should show verification success page" ); // Check DB let verified: bool = sqlx::query_scalar("SELECT email_verified FROM users WHERE id = $1") .bind(user_id) .fetch_one(&h.db) .await .unwrap(); assert!(verified, "email_verified should be true after verification"); } #[tokio::test] async fn email_verification_already_verified() { let mut h = TestHarness::new().await; let user_id = h .signup("alreadyv", "alreadyv@test.com", "password123") .await; // Set email_verified = true so we can test the "already verified" path sqlx::query("UPDATE users SET email_verified = true WHERE id = $1") .bind(user_id) .execute(&h.db) .await .unwrap(); // Generate URL and hit it — should redirect to dashboard (not error) let url = makenotwork::email::generate_verification_url( "", user_id, "alreadyv@test.com", SIGNING_SECRET, ); let resp = h.client.get(&url).await; // Already verified → redirect to /dashboard (302/303) assert!( resp.status.is_redirection(), "Already verified should redirect, got {} {}", resp.status, resp.text ); } #[tokio::test] async fn login_link() { let mut h = TestHarness::new().await; let user_id = h .signup("linkuser", "linkuser@test.com", "password123") .await; // Generate a one-time login token let (token, token_hash) = makenotwork::email::generate_login_token(); // Store it in the DB via direct SQL (db::auth is pub(crate)) let expires_at = chrono::Utc::now() + chrono::Duration::minutes(15); sqlx::query("INSERT INTO login_tokens (user_id, token_hash, expires_at) VALUES ($1, $2, $3)") .bind(user_id) .bind(&token_hash) .bind(expires_at) .execute(&h.db) .await .expect("Failed to create login token"); // Logout first h.client.post_form("/logout", "").await; // Verify we're logged out let resp = h.client.get("/dashboard").await; assert!( resp.status == 302 || resp.status == 303 || resp.status == 401, "Should be logged out, got {}", resp.status ); // Use the login link let resp = h.client.get(&format!("/login-link?token={}", token)).await; assert!( resp.status.is_success() || resp.status.is_redirection(), "Login link failed: {} {}", resp.status, resp.text ); // Verify we're logged in let resp = h.client.get("/dashboard").await; assert_eq!( resp.status, 200, "Should be logged in after login link, got {}", resp.status ); } #[tokio::test] async fn resend_verification_when_unverified() { let mut h = TestHarness::new().await; let user_id = h .signup("resenduser", "resend@test.com", "password123") .await; // Set email_verified to false so we can test resend sqlx::query("UPDATE users SET email_verified = false WHERE id = $1") .bind(user_id) .execute(&h.db) .await .unwrap(); // Resend verification — should succeed (email logged in dev mode) let resp = h.client.post_form("/api/resend-verification", "").await; assert!( resp.status.is_success(), "Resend verification failed: {} {}", resp.status, resp.text ); // Now verify the email directly in DB sqlx::query("UPDATE users SET email_verified = true WHERE id = $1") .bind(user_id) .execute(&h.db) .await .unwrap(); // Resend again — should return "already verified" info (still 200, not an error) let resp = h.client.post_form("/api/resend-verification", "").await; assert!( resp.status.is_success(), "Resend when verified should still succeed: {} {}", resp.status, resp.text ); }