From b5f0f448d7cc85daee5deda59bbd7136dff6bc6a Mon Sep 17 00:00:00 2001 From: Alphonse Paix Date: Wed, 17 Sep 2025 14:16:27 +0200 Subject: [PATCH] Test suite refactoring to match new htmx HTML swapping in pages --- src/routes/admin.rs | 9 ++++- src/routes/subscriptions.rs | 1 - tests/api/admin_dashboard.rs | 6 +-- tests/api/change_password.rs | 33 +++++----------- tests/api/helpers.rs | 60 ++++++---------------------- tests/api/newsletters.rs | 63 ++++++++++++------------------ tests/api/subscriptions.rs | 63 +++++++++++++----------------- tests/api/subscriptions_confirm.rs | 19 ++++----- 8 files changed, 91 insertions(+), 163 deletions(-) diff --git a/src/routes/admin.rs b/src/routes/admin.rs index d4aa6a0..91e7591 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -7,7 +7,8 @@ use crate::{routes::error_chain_fmt, templates::ErrorTemplate}; use askama::Template; use axum::{ Json, - response::{Html, IntoResponse, Redirect, Response}, + http::HeaderMap, + response::{Html, IntoResponse, Response}, }; pub use change_password::*; pub use dashboard::*; @@ -52,7 +53,11 @@ impl IntoResponse for AdminError { }), ) .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) => { let template = ErrorTemplate { error_message: e.to_owned(), diff --git a/src/routes/subscriptions.rs b/src/routes/subscriptions.rs index 6ba03df..4fd5966 100644 --- a/src/routes/subscriptions.rs +++ b/src/routes/subscriptions.rs @@ -209,7 +209,6 @@ Click here to confirm your subscription.", } #[derive(Debug, Deserialize)] -#[allow(dead_code)] pub struct SubscriptionFormData { email: String, } diff --git a/tests/api/admin_dashboard.rs b/tests/api/admin_dashboard.rs index 2da09b3..cbae7dc 100644 --- a/tests/api/admin_dashboard.rs +++ b/tests/api/admin_dashboard.rs @@ -21,14 +21,12 @@ async fn logout_clears_session_state() { assert_is_redirect_to(&response, "/admin/dashboard"); 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; 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; assert_is_redirect_to(&response, "/login"); } diff --git a/tests/api/change_password.rs b/tests/api/change_password.rs index 82b4438..2399b77 100644 --- a/tests/api/change_password.rs +++ b/tests/api/change_password.rs @@ -1,15 +1,5 @@ -use uuid::Uuid; - use crate::helpers::{TestApp, assert_is_redirect_to}; - -#[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"); -} +use uuid::Uuid; #[tokio::test] 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, })) .await; - assert_is_redirect_to(&response, "/admin/password"); + assert!(response.status().is_success()); - let html_page = app.get_change_password_html().await; - assert!(html_page.contains("You entered two different passwords")); + let html_fragment = response.text().await.unwrap(); + assert!(html_fragment.contains("You entered two different passwords")); } #[tokio::test] @@ -70,10 +60,10 @@ async fn current_password_is_invalid() { "new_password_check": new_password, })) .await; - assert_is_redirect_to(&response, "/admin/password"); + assert!(response.status().is_success()); - let html_page = app.get_change_password_html().await; - assert!(html_page.contains("The current password is incorrect")); + let html_fragment = response.text().await.unwrap(); + assert!(html_fragment.contains("The current password is incorrect")); } #[tokio::test] @@ -95,17 +85,14 @@ async fn changing_password_works() { "new_password_check": new_password, })) .await; - assert_is_redirect_to(&response, "/admin/password"); + assert!(response.status().is_success()); - let html_page = app.get_change_password_html().await; - assert!(html_page.contains("Your password has been changed")); + let html_page_fragment = response.text().await.unwrap(); + assert!(html_page_fragment.contains("Your password has been changed")); let response = app.post_logout().await; 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!({ "username": app.test_user.username, "password": new_password, diff --git a/tests/api/helpers.rs b/tests/api/helpers.rs index 49eb5a7..5f16af8 100644 --- a/tests/api/helpers.rs +++ b/tests/api/helpers.rs @@ -6,7 +6,10 @@ use linkify::LinkFinder; use once_cell::sync::Lazy; use sqlx::{Connection, Executor, PgConnection, PgPool}; use uuid::Uuid; -use wiremock::MockServer; +use wiremock::{ + Mock, MockBuilder, MockServer, + matchers::{method, path}, +}; use zero2prod::{ configuration::{DatabaseSettings, get_configuration}, email_client::EmailClient, @@ -149,17 +152,6 @@ impl TestApp { 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 { self.api_client .get(format!("{}/admin/dashboard", &self.address)) @@ -172,29 +164,6 @@ impl TestApp { 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 { self.api_client .post(format!("{}/subscriptions", self.address)) @@ -205,18 +174,6 @@ impl TestApp { .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(&self, body: &Body) -> reqwest::Response where 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) { - assert_eq!(response.status().as_u16(), 303); - assert_eq!(response.headers().get("Location").unwrap(), location); + dbg!(&response); + 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")) } diff --git a/tests/api/newsletters.rs b/tests/api/newsletters.rs index 77d9aa6..d4c1584 100644 --- a/tests/api/newsletters.rs +++ b/tests/api/newsletters.rs @@ -1,14 +1,8 @@ -use crate::helpers::{ConfirmationLinks, TestApp, assert_is_redirect_to}; -use fake::{ - Fake, - faker::{internet::en::SafeEmail, name::fr_fr::Name}, -}; +use crate::helpers::{ConfirmationLinks, TestApp, assert_is_redirect_to, when_sending_an_email}; +use fake::{Fake, faker::internet::en::SafeEmail}; use std::time::Duration; use uuid::Uuid; -use wiremock::{ - Mock, MockBuilder, ResponseTemplate, - matchers::{method, path}, -}; +use wiremock::ResponseTemplate; #[tokio::test] 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; - assert_is_redirect_to(&response, "/admin/newsletters"); + assert!(response.status().is_success()); - let html_page = app.get_newsletter_form_html().await; - assert!(html_page.contains(&format!( - "The newsletter issue '{}' has been published", + let html_fragment = response.text().await.unwrap(); + assert!(html_fragment.contains(&format!( + r#"The newsletter issue "{}" has been published"#, newsletter_title ))); @@ -116,9 +110,13 @@ async fn form_shows_error_for_invalid_data() { ]; for (invalid_body, error_message) in test_cases { - app.post_newsletters(&invalid_body).await; - let html_page = app.get_newsletter_form_html().await; - assert!(html_page.contains(error_message)); + let html_fragment = app + .post_newsletters(&invalid_body) + .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; - assert_is_redirect_to(&response, "/admin/newsletters"); + assert!(response.status().is_success()); - let html_page = app.get_newsletter_form_html().await; - assert!(html_page.contains(&format!( - "The newsletter issue '{}' has been published", + let html_fragment = response.text().await.unwrap(); + assert!(html_fragment.contains(&format!( + r#"The newsletter issue "{}" has been published"#, newsletter_title ))); 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; - assert!(html_page.contains(&format!( - "The newsletter issue '{}' has been published", + let html_fragment = response.text().await.unwrap(); + assert!(html_fragment.contains(&format!( + r#"The newsletter issue "{}" has been published"#, newsletter_title ))); @@ -195,22 +193,16 @@ async fn concurrent_form_submission_is_handled_gracefully() { } async fn create_unconfirmed_subscriber(app: &TestApp) -> ConfirmationLinks { - let name: String = Name().fake(); let email: String = SafeEmail().fake(); - let body = serde_urlencoded::to_string(serde_json::json!({ - "name": name, - "email": email, - "email_check": email - })) - .unwrap(); + let body = format!("email={email}"); - let _mock_guard = Mock::given(path("/v1/email")) - .and(method("POST")) + let _mock_guard = when_sending_an_email() .respond_with(ResponseTemplate::new(200)) .named("Create unconfirmed subscriber") .expect(1) .mount_as_scoped(&app.email_server) .await; + app.post_subscriptions(body) .await .error_for_status() @@ -223,6 +215,7 @@ async fn create_unconfirmed_subscriber(app: &TestApp) -> ConfirmationLinks { .unwrap() .pop() .unwrap(); + app.get_confirmation_links(email_request) } @@ -234,7 +227,3 @@ async fn create_confirmed_subscriber(app: &TestApp) { .error_for_status() .unwrap(); } - -fn when_sending_an_email() -> MockBuilder { - Mock::given(path("/v1/email")).and(method("POST")) -} diff --git a/tests/api/subscriptions.rs b/tests/api/subscriptions.rs index 1b560dc..c81801e 100644 --- a/tests/api/subscriptions.rs +++ b/tests/api/subscriptions.rs @@ -1,34 +1,11 @@ -use crate::helpers::{TestApp, assert_is_redirect_to}; -use wiremock::{ - Mock, ResponseTemplate, - matchers::{method, path}, -}; +use crate::helpers::{TestApp, when_sending_an_email}; +use wiremock::ResponseTemplate; #[tokio::test] async fn subscribe_displays_a_confirmation_message_for_valid_form_data() { let app = TestApp::spawn().await; - Mock::given(path("/v1/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")) + when_sending_an_email() .respond_with(ResponseTemplate::new(200)) .mount(&app.email_server) .await; @@ -37,9 +14,27 @@ async fn subscribe_persists_the_new_subscriber() { let body = format!("email={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")); + assert!(response.status().is_success()); + let html_fragment = response.text().await.unwrap(); + 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") .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 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")) - .and(method("POST")) + when_sending_an_email() .respond_with(ResponseTemplate::new(200)) .expect(1) .mount(&app.email_server) @@ -117,10 +111,9 @@ async fn subscribe_sends_a_confirmation_email_with_a_link() { let app = TestApp::spawn().await; 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")) - .and(method("POST")) + when_sending_an_email() .respond_with(ResponseTemplate::new(200)) .expect(1) .mount(&app.email_server) diff --git a/tests/api/subscriptions_confirm.rs b/tests/api/subscriptions_confirm.rs index 9fedcca..520bfc5 100644 --- a/tests/api/subscriptions_confirm.rs +++ b/tests/api/subscriptions_confirm.rs @@ -1,8 +1,5 @@ -use crate::helpers::TestApp; -use wiremock::{ - Mock, ResponseTemplate, - matchers::{method, path}, -}; +use crate::helpers::{TestApp, when_sending_an_email}; +use wiremock::ResponseTemplate; #[tokio::test] 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] -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 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")) - .and(method("POST")) + when_sending_an_email() .respond_with(ResponseTemplate::new(200)) .expect(1) .mount(&app.email_server) @@ -39,7 +35,7 @@ async fn clicking_on_the_link_shows_a_confiramtion_message() { .text() .await .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 body = format!("email={email}"); - Mock::given(path("v1/email")) - .and(method("POST")) + when_sending_an_email() .respond_with(ResponseTemplate::new(200)) .expect(1) .mount(&app.email_server)