Test suite refactoring to match new htmx HTML swapping in pages

This commit is contained in:
Alphonse Paix
2025-09-17 14:16:27 +02:00
parent 7364e2a23c
commit bdddf0fe4a
8 changed files with 91 additions and 163 deletions

View File

@@ -7,7 +7,8 @@ use crate::{routes::error_chain_fmt, templates::ErrorTemplate};
use askama::Template; use askama::Template;
use axum::{ use axum::{
Json, Json,
response::{Html, IntoResponse, Redirect, Response}, http::HeaderMap,
response::{Html, IntoResponse, Response},
}; };
pub use change_password::*; pub use change_password::*;
pub use dashboard::*; pub use dashboard::*;
@@ -52,7 +53,11 @@ impl IntoResponse for AdminError {
}), }),
) )
.into_response(), .into_response(),
AdminError::NotAuthenticated => Redirect::to("/login").into_response(), AdminError::NotAuthenticated => {
let mut headers = HeaderMap::new();
headers.insert("HX-Redirect", "/login".parse().unwrap());
(StatusCode::OK, headers).into_response()
}
AdminError::ChangePassword(e) => { AdminError::ChangePassword(e) => {
let template = ErrorTemplate { let template = ErrorTemplate {
error_message: e.to_owned(), error_message: e.to_owned(),

View File

@@ -209,7 +209,6 @@ Click <a href=\"{}\">here</a> to confirm your subscription.",
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct SubscriptionFormData { pub struct SubscriptionFormData {
email: String, email: String,
} }

View File

@@ -21,14 +21,12 @@ async fn logout_clears_session_state() {
assert_is_redirect_to(&response, "/admin/dashboard"); assert_is_redirect_to(&response, "/admin/dashboard");
let html_page = app.get_admin_dashboard_html().await; let html_page = app.get_admin_dashboard_html().await;
assert!(html_page.contains(&format!("Welcome {}", app.test_user.username))); assert!(html_page.contains("Connected as"));
assert!(html_page.contains(&app.test_user.username));
let response = app.post_logout().await; let response = app.post_logout().await;
assert_is_redirect_to(&response, "/login"); assert_is_redirect_to(&response, "/login");
let html_page = app.get_login_html().await;
assert!(html_page.contains("You have successfully logged out"));
let response = app.get_admin_dashboard().await; let response = app.get_admin_dashboard().await;
assert_is_redirect_to(&response, "/login"); assert_is_redirect_to(&response, "/login");
} }

View File

@@ -1,15 +1,5 @@
use uuid::Uuid;
use crate::helpers::{TestApp, assert_is_redirect_to}; use crate::helpers::{TestApp, assert_is_redirect_to};
use uuid::Uuid;
#[tokio::test]
async fn you_must_be_logged_in_to_see_the_change_password_form() {
let app = TestApp::spawn().await;
let response = app.get_change_password().await;
assert_is_redirect_to(&response, "/login");
}
#[tokio::test] #[tokio::test]
async fn you_must_be_logged_in_to_change_your_password() { async fn you_must_be_logged_in_to_change_your_password() {
@@ -46,10 +36,10 @@ async fn new_password_fields_must_match() {
"new_password_check": another_new_password, "new_password_check": another_new_password,
})) }))
.await; .await;
assert_is_redirect_to(&response, "/admin/password"); assert!(response.status().is_success());
let html_page = app.get_change_password_html().await; let html_fragment = response.text().await.unwrap();
assert!(html_page.contains("You entered two different passwords")); assert!(html_fragment.contains("You entered two different passwords"));
} }
#[tokio::test] #[tokio::test]
@@ -70,10 +60,10 @@ async fn current_password_is_invalid() {
"new_password_check": new_password, "new_password_check": new_password,
})) }))
.await; .await;
assert_is_redirect_to(&response, "/admin/password"); assert!(response.status().is_success());
let html_page = app.get_change_password_html().await; let html_fragment = response.text().await.unwrap();
assert!(html_page.contains("The current password is incorrect")); assert!(html_fragment.contains("The current password is incorrect"));
} }
#[tokio::test] #[tokio::test]
@@ -95,17 +85,14 @@ async fn changing_password_works() {
"new_password_check": new_password, "new_password_check": new_password,
})) }))
.await; .await;
assert_is_redirect_to(&response, "/admin/password"); assert!(response.status().is_success());
let html_page = app.get_change_password_html().await; let html_page_fragment = response.text().await.unwrap();
assert!(html_page.contains("Your password has been changed")); assert!(html_page_fragment.contains("Your password has been changed"));
let response = app.post_logout().await; let response = app.post_logout().await;
assert_is_redirect_to(&response, "/login"); assert_is_redirect_to(&response, "/login");
let html_page = app.get_login_html().await;
assert!(html_page.contains("You have successfully logged out"));
let login_body = &serde_json::json!({ let login_body = &serde_json::json!({
"username": app.test_user.username, "username": app.test_user.username,
"password": new_password, "password": new_password,

View File

@@ -6,7 +6,10 @@ use linkify::LinkFinder;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use sqlx::{Connection, Executor, PgConnection, PgPool}; use sqlx::{Connection, Executor, PgConnection, PgPool};
use uuid::Uuid; use uuid::Uuid;
use wiremock::MockServer; use wiremock::{
Mock, MockBuilder, MockServer,
matchers::{method, path},
};
use zero2prod::{ use zero2prod::{
configuration::{DatabaseSettings, get_configuration}, configuration::{DatabaseSettings, get_configuration},
email_client::EmailClient, email_client::EmailClient,
@@ -149,17 +152,6 @@ impl TestApp {
ConfirmationLinks { html, text } ConfirmationLinks { html, text }
} }
pub async fn get_login_html(&self) -> String {
self.api_client
.get(format!("{}/login", &self.address))
.send()
.await
.expect("Failed to execute request")
.text()
.await
.unwrap()
}
pub async fn get_admin_dashboard(&self) -> reqwest::Response { pub async fn get_admin_dashboard(&self) -> reqwest::Response {
self.api_client self.api_client
.get(format!("{}/admin/dashboard", &self.address)) .get(format!("{}/admin/dashboard", &self.address))
@@ -172,29 +164,6 @@ impl TestApp {
self.get_admin_dashboard().await.text().await.unwrap() self.get_admin_dashboard().await.text().await.unwrap()
} }
pub async fn get_register_html(&self) -> String {
self.api_client
.get(format!("{}/register", &self.address))
.send()
.await
.expect("Failed to execute request")
.text()
.await
.unwrap()
}
pub async fn get_change_password(&self) -> reqwest::Response {
self.api_client
.get(format!("{}/admin/password", &self.address))
.send()
.await
.expect("Failed to execute request")
}
pub async fn get_change_password_html(&self) -> String {
self.get_change_password().await.text().await.unwrap()
}
pub async fn post_subscriptions(&self, body: String) -> reqwest::Response { pub async fn post_subscriptions(&self, body: String) -> reqwest::Response {
self.api_client self.api_client
.post(format!("{}/subscriptions", self.address)) .post(format!("{}/subscriptions", self.address))
@@ -205,18 +174,6 @@ impl TestApp {
.expect("Failed to execute request") .expect("Failed to execute request")
} }
pub async fn get_newsletter_form(&self) -> reqwest::Response {
self.api_client
.get(format!("{}/admin/password", &self.address))
.send()
.await
.expect("Failed to execute request")
}
pub async fn get_newsletter_form_html(&self) -> String {
self.get_newsletter_form().await.text().await.unwrap()
}
pub async fn post_newsletters<Body>(&self, body: &Body) -> reqwest::Response pub async fn post_newsletters<Body>(&self, body: &Body) -> reqwest::Response
where where
Body: serde::Serialize, Body: serde::Serialize,
@@ -291,6 +248,11 @@ async fn configure_database(config: &DatabaseSettings) -> PgPool {
} }
pub fn assert_is_redirect_to(response: &reqwest::Response, location: &str) { pub fn assert_is_redirect_to(response: &reqwest::Response, location: &str) {
assert_eq!(response.status().as_u16(), 303); dbg!(&response);
assert_eq!(response.headers().get("Location").unwrap(), location); assert_eq!(response.status().as_u16(), 200);
assert_eq!(response.headers().get("hx-redirect").unwrap(), location);
}
pub fn when_sending_an_email() -> MockBuilder {
Mock::given(path("/email")).and(method("POST"))
} }

View File

@@ -1,14 +1,8 @@
use crate::helpers::{ConfirmationLinks, TestApp, assert_is_redirect_to}; use crate::helpers::{ConfirmationLinks, TestApp, assert_is_redirect_to, when_sending_an_email};
use fake::{ use fake::{Fake, faker::internet::en::SafeEmail};
Fake,
faker::{internet::en::SafeEmail, name::fr_fr::Name},
};
use std::time::Duration; use std::time::Duration;
use uuid::Uuid; use uuid::Uuid;
use wiremock::{ use wiremock::ResponseTemplate;
Mock, MockBuilder, ResponseTemplate,
matchers::{method, path},
};
#[tokio::test] #[tokio::test]
async fn newsletters_are_not_delivered_to_unconfirmed_subscribers() { async fn newsletters_are_not_delivered_to_unconfirmed_subscribers() {
@@ -72,11 +66,11 @@ async fn newsletters_are_delivered_to_confirmed_subscribers() {
}); });
let response = app.post_newsletters(&newsletter_request_body).await; let response = app.post_newsletters(&newsletter_request_body).await;
assert_is_redirect_to(&response, "/admin/newsletters"); assert!(response.status().is_success());
let html_page = app.get_newsletter_form_html().await; let html_fragment = response.text().await.unwrap();
assert!(html_page.contains(&format!( assert!(html_fragment.contains(&format!(
"The newsletter issue '{}' has been published", r#"The newsletter issue &#34;{}&#34; has been published"#,
newsletter_title newsletter_title
))); )));
@@ -116,9 +110,13 @@ async fn form_shows_error_for_invalid_data() {
]; ];
for (invalid_body, error_message) in test_cases { for (invalid_body, error_message) in test_cases {
app.post_newsletters(&invalid_body).await; let html_fragment = app
let html_page = app.get_newsletter_form_html().await; .post_newsletters(&invalid_body)
assert!(html_page.contains(error_message)); .await
.text()
.await
.unwrap();
assert!(html_fragment.contains(error_message));
} }
} }
@@ -143,20 +141,20 @@ async fn newsletter_creation_is_idempotent() {
}); });
let response = app.post_newsletters(&newsletter_request_body).await; let response = app.post_newsletters(&newsletter_request_body).await;
assert_is_redirect_to(&response, "/admin/newsletters"); assert!(response.status().is_success());
let html_page = app.get_newsletter_form_html().await; let html_fragment = response.text().await.unwrap();
assert!(html_page.contains(&format!( assert!(html_fragment.contains(&format!(
"The newsletter issue '{}' has been published", r#"The newsletter issue &#34;{}&#34; has been published"#,
newsletter_title newsletter_title
))); )));
let response = app.post_newsletters(&newsletter_request_body).await; let response = app.post_newsletters(&newsletter_request_body).await;
assert_is_redirect_to(&response, "/admin/newsletters"); assert!(response.status().is_success());
let html_page = app.get_newsletter_form_html().await; let html_fragment = response.text().await.unwrap();
assert!(html_page.contains(&format!( assert!(html_fragment.contains(&format!(
"The newsletter issue '{}' has been published", r#"The newsletter issue &#34;{}&#34; has been published"#,
newsletter_title newsletter_title
))); )));
@@ -195,22 +193,16 @@ async fn concurrent_form_submission_is_handled_gracefully() {
} }
async fn create_unconfirmed_subscriber(app: &TestApp) -> ConfirmationLinks { async fn create_unconfirmed_subscriber(app: &TestApp) -> ConfirmationLinks {
let name: String = Name().fake();
let email: String = SafeEmail().fake(); let email: String = SafeEmail().fake();
let body = serde_urlencoded::to_string(serde_json::json!({ let body = format!("email={email}");
"name": name,
"email": email,
"email_check": email
}))
.unwrap();
let _mock_guard = Mock::given(path("/v1/email")) let _mock_guard = when_sending_an_email()
.and(method("POST"))
.respond_with(ResponseTemplate::new(200)) .respond_with(ResponseTemplate::new(200))
.named("Create unconfirmed subscriber") .named("Create unconfirmed subscriber")
.expect(1) .expect(1)
.mount_as_scoped(&app.email_server) .mount_as_scoped(&app.email_server)
.await; .await;
app.post_subscriptions(body) app.post_subscriptions(body)
.await .await
.error_for_status() .error_for_status()
@@ -223,6 +215,7 @@ async fn create_unconfirmed_subscriber(app: &TestApp) -> ConfirmationLinks {
.unwrap() .unwrap()
.pop() .pop()
.unwrap(); .unwrap();
app.get_confirmation_links(email_request) app.get_confirmation_links(email_request)
} }
@@ -234,7 +227,3 @@ async fn create_confirmed_subscriber(app: &TestApp) {
.error_for_status() .error_for_status()
.unwrap(); .unwrap();
} }
fn when_sending_an_email() -> MockBuilder {
Mock::given(path("/v1/email")).and(method("POST"))
}

View File

@@ -1,34 +1,11 @@
use crate::helpers::{TestApp, assert_is_redirect_to}; use crate::helpers::{TestApp, when_sending_an_email};
use wiremock::{ use wiremock::ResponseTemplate;
Mock, ResponseTemplate,
matchers::{method, path},
};
#[tokio::test] #[tokio::test]
async fn subscribe_displays_a_confirmation_message_for_valid_form_data() { async fn subscribe_displays_a_confirmation_message_for_valid_form_data() {
let app = TestApp::spawn().await; let app = TestApp::spawn().await;
Mock::given(path("/v1/email")) when_sending_an_email()
.and(method("POST"))
.respond_with(ResponseTemplate::new(200))
.mount(&app.email_server)
.await;
let email = "alphonse.paix@outlook.com";
let body = format!("name=Alphonse&email={0}&email_check={0}", email);
let response = app.post_subscriptions(body).await;
assert_is_redirect_to(&response, "/register");
let page_html = app.get_register_html().await;
assert!(page_html.contains("A confirmation email has been sent"));
}
#[tokio::test]
async fn subscribe_persists_the_new_subscriber() {
let app = TestApp::spawn().await;
Mock::given(path("/v1/email"))
.and(method("POST"))
.respond_with(ResponseTemplate::new(200)) .respond_with(ResponseTemplate::new(200))
.mount(&app.email_server) .mount(&app.email_server)
.await; .await;
@@ -37,9 +14,27 @@ async fn subscribe_persists_the_new_subscriber() {
let body = format!("email={email}"); let body = format!("email={email}");
let response = app.post_subscriptions(body).await; let response = app.post_subscriptions(body).await;
assert_is_redirect_to(&response, "/register"); assert!(response.status().is_success());
let page_html = app.get_register_html().await; let html_fragment = response.text().await.unwrap();
assert!(page_html.contains("A confirmation email has been sent")); assert!(html_fragment.contains("A confirmation email has been sent"));
}
#[tokio::test]
async fn subscribe_persists_the_new_subscriber() {
let app = TestApp::spawn().await;
when_sending_an_email()
.respond_with(ResponseTemplate::new(200))
.mount(&app.email_server)
.await;
let email = "alphonse.paix@outlook.com";
let body = format!("email={email}");
let response = app.post_subscriptions(body).await;
assert!(response.status().is_success());
let html_fragment = response.text().await.unwrap();
assert!(html_fragment.contains("A confirmation email has been sent"));
let saved = sqlx::query!("SELECT email, status FROM subscriptions") let saved = sqlx::query!("SELECT email, status FROM subscriptions")
.fetch_one(&app.connection_pool) .fetch_one(&app.connection_pool)
@@ -100,10 +95,9 @@ async fn subscribe_sends_a_confirmation_email_for_valid_data() {
let app = TestApp::spawn().await; let app = TestApp::spawn().await;
let email = "alphonse.paix@outlook.com"; let email = "alphonse.paix@outlook.com";
let body = format!("name=Alphonse&email={0}&email_check={0}", email); let body = format!("email={email}");
Mock::given(path("v1/email")) when_sending_an_email()
.and(method("POST"))
.respond_with(ResponseTemplate::new(200)) .respond_with(ResponseTemplate::new(200))
.expect(1) .expect(1)
.mount(&app.email_server) .mount(&app.email_server)
@@ -117,10 +111,9 @@ async fn subscribe_sends_a_confirmation_email_with_a_link() {
let app = TestApp::spawn().await; let app = TestApp::spawn().await;
let email = "alphonse.paix@outlook.com"; let email = "alphonse.paix@outlook.com";
let body = format!("name=Alphonse&email={0}&email_check={0}", email); let body = format!("email={email}");
Mock::given(path("v1/email")) when_sending_an_email()
.and(method("POST"))
.respond_with(ResponseTemplate::new(200)) .respond_with(ResponseTemplate::new(200))
.expect(1) .expect(1)
.mount(&app.email_server) .mount(&app.email_server)

View File

@@ -1,8 +1,5 @@
use crate::helpers::TestApp; use crate::helpers::{TestApp, when_sending_an_email};
use wiremock::{ use wiremock::ResponseTemplate;
Mock, ResponseTemplate,
matchers::{method, path},
};
#[tokio::test] #[tokio::test]
async fn confirmation_links_without_token_are_rejected_with_a_400() { async fn confirmation_links_without_token_are_rejected_with_a_400() {
@@ -15,14 +12,13 @@ async fn confirmation_links_without_token_are_rejected_with_a_400() {
} }
#[tokio::test] #[tokio::test]
async fn clicking_on_the_link_shows_a_confiramtion_message() { async fn clicking_on_the_link_shows_a_confirmation_message() {
let app = TestApp::spawn().await; let app = TestApp::spawn().await;
let email = "alphonse.paix@outlook.com"; let email = "alphonse.paix@outlook.com";
let body = format!("name=Alphonse&email={email}&email_check={email}"); let body = format!("email={email}");
Mock::given(path("v1/email")) when_sending_an_email()
.and(method("POST"))
.respond_with(ResponseTemplate::new(200)) .respond_with(ResponseTemplate::new(200))
.expect(1) .expect(1)
.mount(&app.email_server) .mount(&app.email_server)
@@ -39,7 +35,7 @@ async fn clicking_on_the_link_shows_a_confiramtion_message() {
.text() .text()
.await .await
.unwrap() .unwrap()
.contains("Your account has been confirmed") .contains("Subscription confirmed")
); );
} }
@@ -50,8 +46,7 @@ async fn clicking_on_the_confirmation_link_confirms_a_subscriber() {
let email = "alphonse.paix@outlook.com"; let email = "alphonse.paix@outlook.com";
let body = format!("email={email}"); let body = format!("email={email}");
Mock::given(path("v1/email")) when_sending_an_email()
.and(method("POST"))
.respond_with(ResponseTemplate::new(200)) .respond_with(ResponseTemplate::new(200))
.expect(1) .expect(1)
.mount(&app.email_server) .mount(&app.email_server)