From 3bfac6d012788623fbca11501674aea26901af87 Mon Sep 17 00:00:00 2001 From: Alphonse Paix Date: Tue, 7 Oct 2025 23:07:16 +0200 Subject: [PATCH] Profile update tests --- src/routes/posts.rs | 5 +- src/routes/users.rs | 29 ++++++- tests/api/helpers.rs | 32 ++++++++ tests/api/users.rs | 181 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 5 deletions(-) diff --git a/src/routes/posts.rs b/src/routes/posts.rs index c8f48c5..bf89ce8 100644 --- a/src/routes/posts.rs +++ b/src/routes/posts.rs @@ -158,7 +158,10 @@ pub async fn update_post( )) .into_response()) } - _ => Ok(HtmlTemplate(ErrorTemplate::Forbidden).into_response()), + _ => Ok(HtmlTemplate(MessageTemplate::error( + "You are not authorized. Only the author can edit his post.".into(), + )) + .into_response()), } } diff --git a/src/routes/users.rs b/src/routes/users.rs index c37bf15..7365390 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -1,7 +1,7 @@ use crate::authentication::AuthenticatedUser; use crate::routes::verify_password; use crate::session_state::TypedSession; -use crate::templates::{ErrorTemplate, MessageTemplate, UserEditTemplate}; +use crate::templates::{MessageTemplate, UserEditTemplate}; use crate::{ authentication::Role, domain::{PostEntry, UserEntry}, @@ -18,6 +18,7 @@ use axum::{ use secrecy::{ExposeSecret, SecretString}; use sqlx::PgPool; use uuid::Uuid; +use validator::Validate; pub async fn user_edit_form( Extension(AuthenticatedUser { user_id, .. }): Extension, @@ -41,9 +42,10 @@ pub async fn user_edit_form( Ok(template.into_response()) } -#[derive(serde::Deserialize)] +#[derive(Debug, Validate, serde::Deserialize)] pub struct EditProfileForm { user_id: Uuid, + #[validate(length(min = 3, message = "Username must be at least 3 characters."))] username: String, full_name: String, bio: String, @@ -62,8 +64,27 @@ pub async fn update_user( }): Extension, Form(form): Form, ) -> Result { + if let Err(e) = form.validate() { + let error_messages: Vec<_> = e + .field_errors() + .iter() + .flat_map(|(field, errors)| { + errors.iter().map(move |error| { + error + .message + .as_ref() + .map(|msg| msg.to_string()) + .unwrap_or(format!("Invalid field: {}", field)) + }) + }) + .collect(); + let template = HtmlTemplate(MessageTemplate::error(error_messages.join("\n"))); + return Ok(template.into_response()); + } if form.user_id != session_user_id { - let template = HtmlTemplate(ErrorTemplate::Forbidden); + let template = HtmlTemplate(MessageTemplate::error( + "You are not authorized. Refresh the page and try again.".into(), + )); return Ok(template.into_response()); } let updated_username = form.username.trim(); @@ -78,7 +99,7 @@ pub async fn update_user( .is_some() { let template = HtmlTemplate(MessageTemplate::error( - "The username is already taken.".into(), + "This username is already taken.".into(), )); return Ok(template.into_response()); } diff --git a/tests/api/helpers.rs b/tests/api/helpers.rs index bcfdf4e..59483d5 100644 --- a/tests/api/helpers.rs +++ b/tests/api/helpers.rs @@ -375,6 +375,30 @@ impl TestApp { .expect("Failed to execute request") } + pub async fn edit_profile(&self, body: &Body) -> reqwest::Response + where + Body: serde::Serialize, + { + self.api_client + .put(format!("{}/users/edit", self.address)) + .form(body) + .send() + .await + .expect("Failed to execute request") + } + + pub async fn get_profile(&self, username: &str) -> reqwest::Response { + self.api_client + .get(format!("{}/users/{}", self.address, username)) + .send() + .await + .expect("Failed to execute request") + } + + pub async fn get_profile_html(&self, username: &str) -> String { + self.get_profile(username).await.text().await.unwrap() + } + pub async fn post_create_post(&self, body: &Body) -> reqwest::Response where Body: serde::Serialize, @@ -426,6 +450,14 @@ impl TestApp { .await .expect("Failed to execute request") } + + pub async fn get_user_id(&self, username: &str) -> Uuid { + let record = sqlx::query!("SELECT user_id FROM users WHERE username = $1", username) + .fetch_one(&self.connection_pool) + .await + .unwrap(); + record.user_id + } } pub fn assert_is_redirect_to(response: &reqwest::Response, location: &str) { diff --git a/tests/api/users.rs b/tests/api/users.rs index bdd5c57..eaa098a 100644 --- a/tests/api/users.rs +++ b/tests/api/users.rs @@ -344,3 +344,184 @@ async fn writers_cannot_perform_admin_functions(connection_pool: PgPool) { .unwrap(); assert!(record.is_none()); } + +#[sqlx::test] +async fn user_can_change_his_display_name(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; + app.admin_login().await; + let username = "alphonse"; + let password = "123456789abc"; + app.create_user(username, password, false).await; + let user_id = app.get_user_id(username).await; + let login_body = serde_json::json!({ + "username": username, + "password": password + }); + app.post_login(&login_body).await; + let full_name = "Alphonse Paix"; + let edit_body = serde_json::json!( { + "user_id": user_id, + "username": username, + "full_name": full_name, + "bio": "", + }); + let html = app.get_profile_html(username).await; + assert!(!html.contains(full_name)); + let response = app.edit_profile(&edit_body).await; + assert!(dbg!(response.text().await.unwrap()).contains("Your profile has been updated")); + let html = app.get_profile_html(username).await; + assert!(html.contains(full_name)); +} + +#[sqlx::test] +async fn user_can_change_his_bio(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; + app.admin_login().await; + let username = "alphonse"; + let password = "123456789abc"; + app.create_user(username, password, false).await; + let user_id = app.get_user_id(username).await; + let login_body = serde_json::json!({ + "username": username, + "password": password + }); + app.post_login(&login_body).await; + let bio = "This is me"; + let edit_body = serde_json::json!( { + "user_id": user_id, + "username": username, + "full_name": "", + "bio": bio, + }); + let html = app.get_profile_html(username).await; + assert!(!html.contains(bio)); + let response = app.edit_profile(&edit_body).await; + assert!(dbg!(response.text().await.unwrap()).contains("Your profile has been updated")); + let html = app.get_profile_html(username).await; + assert!(html.contains(bio)); +} + +#[sqlx::test] +async fn user_can_change_his_username(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; + app.admin_login().await; + let username = "alphonse"; + let password = "123456789abc"; + app.create_user(username, password, false).await; + let user_id = app.get_user_id(username).await; + let login_body = serde_json::json!({ + "username": username, + "password": password + }); + app.post_login(&login_body).await; + let new_username = "alphonsepaix"; + let edit_body = serde_json::json!( { + "user_id": user_id, + "username": new_username, + "full_name": "", + "bio": "", + }); + let html = app.get_profile_html(username).await; + assert!(html.contains(username)); + let response = app.edit_profile(&edit_body).await; + assert!(dbg!(response.text().await.unwrap()).contains("Your profile has been updated")); + let html = app.get_profile_html(username).await; + assert!(html.contains("404")); + let html = app.get_profile_html(new_username).await; + assert!(html.contains(new_username)); +} + +#[sqlx::test] +async fn user_cannot_change_other_profiles(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; + app.admin_login().await; + let username = "alphonse"; + let password = "123456789abc"; + app.create_user(username, password, false).await; + let other_user_id = app.get_user_id("admin").await; + let login_body = serde_json::json!({ + "username": username, + "password": password + }); + app.post_login(&login_body).await; + let new_username = "alphonsepaix"; + let edit_body = serde_json::json!( { + "user_id": other_user_id, + "username": new_username, + "full_name": "", + "bio": "", + }); + let response = app.edit_profile(&edit_body).await; + assert!( + response + .text() + .await + .unwrap() + .contains("You are not authorized") + ); +} + +#[sqlx::test] +async fn user_cannot_take_an_existing_username(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; + app.admin_login().await; + let username = "alphonse"; + let password = "123456789abc"; + app.create_user(username, password, false).await; + let user_id = app.get_user_id(username).await; + let login_body = serde_json::json!({ + "username": username, + "password": password + }); + app.post_login(&login_body).await; + let edit_body = serde_json::json!( { + "user_id": user_id, + "username": "admin", + "full_name": "", + "bio": "", + }); + let response = app.edit_profile(&edit_body).await; + assert!( + response + .text() + .await + .unwrap() + .contains("This username is already taken") + ); + let html = app.get_profile_html(username).await; + assert!(html.contains(username)); +} + +#[sqlx::test] +async fn invalid_fields_are_rejected(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; + app.admin_login().await; + let username = "alphonse"; + let password = "123456789abc"; + app.create_user(username, password, false).await; + let user_id = app.get_user_id(username).await; + let login_body = serde_json::json!({ + "username": username, + "password": password + }); + app.post_login(&login_body).await; + + let test_cases = [( + serde_json::json!({ + "user_id": user_id, + "username": "ab", + "full_name": "", + "bio": "", + }), + "Username must be at least 3 characters", + "the username was too short", + )]; + for (invalid_body, expected_error_message, explaination) in test_cases { + let html = app.edit_profile(&invalid_body).await; + assert!( + html.text().await.unwrap().contains(expected_error_message), + "The API did not reject the changes when {}", + explaination + ); + } +}