Merge branch 'tests'

This commit is contained in:
Alphonse Paix
2025-10-07 23:27:15 +02:00
4 changed files with 242 additions and 5 deletions

View File

@@ -158,7 +158,10 @@ pub async fn update_post(
)) ))
.into_response()) .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()),
} }
} }

View File

@@ -1,7 +1,7 @@
use crate::authentication::AuthenticatedUser; use crate::authentication::AuthenticatedUser;
use crate::routes::verify_password; use crate::routes::verify_password;
use crate::session_state::TypedSession; use crate::session_state::TypedSession;
use crate::templates::{ErrorTemplate, MessageTemplate, UserEditTemplate}; use crate::templates::{MessageTemplate, UserEditTemplate};
use crate::{ use crate::{
authentication::Role, authentication::Role,
domain::{PostEntry, UserEntry}, domain::{PostEntry, UserEntry},
@@ -18,6 +18,7 @@ use axum::{
use secrecy::{ExposeSecret, SecretString}; use secrecy::{ExposeSecret, SecretString};
use sqlx::PgPool; use sqlx::PgPool;
use uuid::Uuid; use uuid::Uuid;
use validator::Validate;
pub async fn user_edit_form( pub async fn user_edit_form(
Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>, Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>,
@@ -41,9 +42,10 @@ pub async fn user_edit_form(
Ok(template.into_response()) Ok(template.into_response())
} }
#[derive(serde::Deserialize)] #[derive(Debug, Validate, serde::Deserialize)]
pub struct EditProfileForm { pub struct EditProfileForm {
user_id: Uuid, user_id: Uuid,
#[validate(length(min = 3, message = "Username must be at least 3 characters."))]
username: String, username: String,
full_name: String, full_name: String,
bio: String, bio: String,
@@ -62,8 +64,27 @@ pub async fn update_user(
}): Extension<AuthenticatedUser>, }): Extension<AuthenticatedUser>,
Form(form): Form<EditProfileForm>, Form(form): Form<EditProfileForm>,
) -> Result<Response, AppError> { ) -> Result<Response, AppError> {
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 { 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()); return Ok(template.into_response());
} }
let updated_username = form.username.trim(); let updated_username = form.username.trim();
@@ -78,7 +99,7 @@ pub async fn update_user(
.is_some() .is_some()
{ {
let template = HtmlTemplate(MessageTemplate::error( let template = HtmlTemplate(MessageTemplate::error(
"The username is already taken.".into(), "This username is already taken.".into(),
)); ));
return Ok(template.into_response()); return Ok(template.into_response());
} }

View File

@@ -375,6 +375,30 @@ impl TestApp {
.expect("Failed to execute request") .expect("Failed to execute request")
} }
pub async fn edit_profile<Body>(&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<Body>(&self, body: &Body) -> reqwest::Response pub async fn post_create_post<Body>(&self, body: &Body) -> reqwest::Response
where where
Body: serde::Serialize, Body: serde::Serialize,
@@ -426,6 +450,14 @@ impl TestApp {
.await .await
.expect("Failed to execute request") .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) { pub fn assert_is_redirect_to(response: &reqwest::Response, location: &str) {

View File

@@ -344,3 +344,184 @@ async fn writers_cannot_perform_admin_functions(connection_pool: PgPool) {
.unwrap(); .unwrap();
assert!(record.is_none()); 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
);
}
}