From 33281132c6a6a79bb04603a0f830c8cb923a7908 Mon Sep 17 00:00:00 2001 From: Alphonse Paix Date: Wed, 24 Sep 2025 02:55:18 +0200 Subject: [PATCH] Update test suite to drop database automatically when test is successfull --- src/email_client.rs | 8 ++--- tests/api/admin_dashboard.rs | 13 ++++---- tests/api/change_password.rs | 25 +++++++------- tests/api/health_check.rs | 7 ++-- tests/api/helpers.rs | 33 +++++-------------- tests/api/login.rs | 13 ++++---- tests/api/newsletters.rs | 52 +++++++++++++----------------- tests/api/posts.rs | 21 ++++++------ tests/api/subscriptions.rs | 37 ++++++++++----------- tests/api/subscriptions_confirm.rs | 19 +++++------ tests/api/unsubscribe.rs | 35 +++++++++----------- tests/api/unsubscribe_confirm.rs | 13 ++++---- 12 files changed, 128 insertions(+), 148 deletions(-) diff --git a/src/email_client.rs b/src/email_client.rs index d5036c6..1e4515c 100644 --- a/src/email_client.rs +++ b/src/email_client.rs @@ -119,7 +119,7 @@ mod tests { EmailClient::build(settings).unwrap() } - #[tokio::test] + #[sqlx::test] async fn send_email_sends_the_expected_request() { let mock_server = MockServer::start().await; let email_client = email_client(mock_server.uri()); @@ -141,7 +141,7 @@ mod tests { .unwrap(); } - #[tokio::test] + #[sqlx::test] async fn send_email_succeeds_if_the_server_returns_200() { let mock_server = MockServer::start().await; let email_client = email_client(mock_server.uri()); @@ -159,7 +159,7 @@ mod tests { assert_ok!(response); } - #[tokio::test] + #[sqlx::test] async fn send_email_fails_if_the_server_retuns_500() { let mock_server = MockServer::start().await; let email_client = email_client(mock_server.uri()); @@ -177,7 +177,7 @@ mod tests { assert_err!(response); } - #[tokio::test] + #[sqlx::test] async fn send_email_times_out_if_the_server_takes_too_long() { let mock_server = MockServer::start().await; let email_client = email_client(mock_server.uri()); diff --git a/tests/api/admin_dashboard.rs b/tests/api/admin_dashboard.rs index cbae7dc..a46adad 100644 --- a/tests/api/admin_dashboard.rs +++ b/tests/api/admin_dashboard.rs @@ -1,17 +1,18 @@ use crate::helpers::{TestApp, assert_is_redirect_to}; +use sqlx::PgPool; -#[tokio::test] -async fn you_must_be_logged_in_to_access_the_admin_dashboard() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn you_must_be_logged_in_to_access_the_admin_dashboard(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let response = app.get_admin_dashboard().await; assert_is_redirect_to(&response, "/login"); } -#[tokio::test] -async fn logout_clears_session_state() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn logout_clears_session_state(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let login_body = serde_json::json!({ "username": &app.test_user.username, diff --git a/tests/api/change_password.rs b/tests/api/change_password.rs index 2399b77..bdbd7f2 100644 --- a/tests/api/change_password.rs +++ b/tests/api/change_password.rs @@ -1,9 +1,10 @@ use crate::helpers::{TestApp, assert_is_redirect_to}; +use sqlx::PgPool; use uuid::Uuid; -#[tokio::test] -async fn you_must_be_logged_in_to_change_your_password() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn you_must_be_logged_in_to_change_your_password(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let new_password = Uuid::new_v4().to_string(); let response = app @@ -17,9 +18,9 @@ async fn you_must_be_logged_in_to_change_your_password() { assert_is_redirect_to(&response, "/login"); } -#[tokio::test] -async fn new_password_fields_must_match() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn new_password_fields_must_match(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.post_login(&serde_json::json!({ "username": app.test_user.username, @@ -42,9 +43,9 @@ async fn new_password_fields_must_match() { assert!(html_fragment.contains("You entered two different passwords")); } -#[tokio::test] -async fn current_password_is_invalid() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn current_password_is_invalid(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.post_login(&serde_json::json!({ "username": app.test_user.username, @@ -66,9 +67,9 @@ async fn current_password_is_invalid() { assert!(html_fragment.contains("The current password is incorrect")); } -#[tokio::test] -async fn changing_password_works() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn changing_password_works(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let login_body = &serde_json::json!({ "username": app.test_user.username, diff --git a/tests/api/health_check.rs b/tests/api/health_check.rs index 70dbd2a..4c4bcef 100644 --- a/tests/api/health_check.rs +++ b/tests/api/health_check.rs @@ -1,8 +1,9 @@ use crate::helpers::TestApp; +use sqlx::PgPool; -#[tokio::test] -async fn health_check_works() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn health_check_works(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let client = reqwest::Client::new(); let response = client diff --git a/tests/api/helpers.rs b/tests/api/helpers.rs index 2df2774..842007f 100644 --- a/tests/api/helpers.rs +++ b/tests/api/helpers.rs @@ -5,14 +5,14 @@ use argon2::{ use fake::{Fake, faker::internet::en::SafeEmail}; use linkify::{Link, LinkFinder}; use once_cell::sync::Lazy; -use sqlx::{Connection, Executor, PgConnection, PgPool}; +use sqlx::PgPool; use uuid::Uuid; use wiremock::{ Mock, MockBuilder, MockServer, ResponseTemplate, matchers::{method, path}, }; use zero2prod::{ - configuration::{DatabaseSettings, get_configuration}, + configuration::get_configuration, email_client::EmailClient, issue_delivery_worker::{ExecutionOutcome, try_execute_task}, startup::Application, @@ -80,19 +80,22 @@ pub struct TestApp { } impl TestApp { - pub async fn spawn() -> Self { + pub async fn spawn(connection_pool: PgPool) -> Self { Lazy::force(&TRACING); let email_server = MockServer::start().await; let configuration = { let mut c = get_configuration().expect("Failed to read configuration"); - c.database.database_name = Uuid::new_v4().to_string(); c.application.port = 0; c.email_client.base_url = email_server.uri(); + c.database.database_name = connection_pool + .connect_options() + .get_database() + .unwrap() + .to_string(); c }; let local_addr = configuration.application.host.clone(); - let connection_pool = configure_database(&configuration.database).await; let email_client = EmailClient::build(configuration.email_client.clone()).unwrap(); let application = Application::build(configuration) .await @@ -312,26 +315,6 @@ impl TestApp { } } -async fn configure_database(config: &DatabaseSettings) -> PgPool { - let mut connection = PgConnection::connect_with(&config.without_db()) - .await - .expect("Failed to connect to Postgres"); - connection - .execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_ref()) - .await - .expect("Failed to create the database"); - - let connection_pool = PgPool::connect_with(config.with_db()) - .await - .expect("Failed to connect to Postgres"); - sqlx::migrate!("./migrations") - .run(&connection_pool) - .await - .expect("Failed to migrate the database"); - - connection_pool -} - pub fn assert_is_redirect_to(response: &reqwest::Response, location: &str) { assert!( response.status().as_u16() == 303 diff --git a/tests/api/login.rs b/tests/api/login.rs index f2f612d..4b02611 100644 --- a/tests/api/login.rs +++ b/tests/api/login.rs @@ -1,8 +1,9 @@ use crate::helpers::{TestApp, assert_is_redirect_to}; +use sqlx::PgPool; -#[tokio::test] -async fn an_error_html_fragment_is_returned_on_failure() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn an_error_html_fragment_is_returned_on_failure(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let login_body = serde_json::json!({ "username": "user", @@ -17,9 +18,9 @@ async fn an_error_html_fragment_is_returned_on_failure() { assert!(response_html.contains("Invalid credentials")); } -#[tokio::test] -async fn login_redirects_to_admin_dashboard_after_login_success() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn login_redirects_to_admin_dashboard_after_login_success(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let login_body = serde_json::json!({ "username": &app.test_user.username, diff --git a/tests/api/newsletters.rs b/tests/api/newsletters.rs index abf8a4d..4404dc0 100644 --- a/tests/api/newsletters.rs +++ b/tests/api/newsletters.rs @@ -1,11 +1,12 @@ use crate::helpers::{TestApp, assert_is_redirect_to, when_sending_an_email}; +use sqlx::PgPool; use std::time::Duration; use uuid::Uuid; use wiremock::ResponseTemplate; -#[tokio::test] -async fn newsletters_are_not_delivered_to_unconfirmed_subscribers() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn newsletters_are_not_delivered_to_unconfirmed_subscribers(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.create_unconfirmed_subscriber().await; app.admin_login().await; @@ -25,9 +26,9 @@ async fn newsletters_are_not_delivered_to_unconfirmed_subscribers() { app.dispatch_all_pending_emails().await; } -#[tokio::test] -async fn requests_without_authentication_are_redirected() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn requests_without_authentication_are_redirected(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; when_sending_an_email() .respond_with(ResponseTemplate::new(200)) @@ -44,9 +45,9 @@ async fn requests_without_authentication_are_redirected() { assert_is_redirect_to(&response, "/login"); } -#[tokio::test] -async fn newsletters_are_delivered_to_confirmed_subscribers() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn newsletters_are_delivered_to_confirmed_subscribers(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.create_confirmed_subscriber().await; app.admin_login().await; @@ -68,17 +69,14 @@ async fn newsletters_are_delivered_to_confirmed_subscribers() { assert!(response.status().is_success()); let html_fragment = response.text().await.unwrap(); - assert!(html_fragment.contains(&format!( - r#"The newsletter issue "{}" has been published"#, - newsletter_title - ))); + assert!(html_fragment.contains("Your email has been queued for delivery")); app.dispatch_all_pending_emails().await; } -#[tokio::test] -async fn form_shows_error_for_invalid_data() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn form_shows_error_for_invalid_data(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.admin_login().await; when_sending_an_email() @@ -119,9 +117,9 @@ async fn form_shows_error_for_invalid_data() { } } -#[tokio::test] -async fn newsletter_creation_is_idempotent() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn newsletter_creation_is_idempotent(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.create_confirmed_subscriber().await; app.admin_login().await; @@ -143,26 +141,20 @@ async fn newsletter_creation_is_idempotent() { assert!(response.status().is_success()); let html_fragment = response.text().await.unwrap(); - assert!(html_fragment.contains(&format!( - r#"The newsletter issue "{}" has been published"#, - newsletter_title - ))); + assert!(html_fragment.contains("Your email has been queued for delivery")); let response = app.post_newsletters(&newsletter_request_body).await; assert!(response.status().is_success()); let html_fragment = response.text().await.unwrap(); - assert!(html_fragment.contains(&format!( - r#"The newsletter issue "{}" has been published"#, - newsletter_title - ))); + assert!(html_fragment.contains("Your email has been queued for delivery")); app.dispatch_all_pending_emails().await; } -#[tokio::test] -async fn concurrent_form_submission_is_handled_gracefully() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn concurrent_form_submission_is_handled_gracefully(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.create_confirmed_subscriber().await; app.admin_login().await; diff --git a/tests/api/posts.rs b/tests/api/posts.rs index 7ac5182..3ae4dee 100644 --- a/tests/api/posts.rs +++ b/tests/api/posts.rs @@ -3,6 +3,7 @@ use fake::{ Fake, faker::lorem::en::{Paragraph, Sentence}, }; +use sqlx::PgPool; use uuid::Uuid; use wiremock::ResponseTemplate; @@ -14,9 +15,9 @@ fn content() -> String { Paragraph(1..10).fake() } -#[tokio::test] -async fn you_must_be_logged_in_to_create_a_new_post() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn you_must_be_logged_in_to_create_a_new_post(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let title = subject(); let content = content(); @@ -29,9 +30,9 @@ async fn you_must_be_logged_in_to_create_a_new_post() { assert_is_redirect_to(&response, "/login"); } -#[tokio::test] -async fn new_posts_are_stored_in_the_database() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn new_posts_are_stored_in_the_database(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.admin_login().await; let title = subject(); @@ -45,7 +46,7 @@ async fn new_posts_are_stored_in_the_database() { assert!(response.status().is_success()); let html_fragment = response.text().await.unwrap(); - assert!(html_fragment.contains("Your new post has been saved")); + assert!(html_fragment.contains("Your new post has been published")); let saved = sqlx::query!("SELECT title, content FROM posts") .fetch_one(&app.connection_pool) @@ -56,9 +57,9 @@ async fn new_posts_are_stored_in_the_database() { assert_eq!(saved.content, content); } -#[tokio::test] -async fn confirmed_subscribers_are_notified_when_a_new_post_is_published() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn confirmed_subscribers_are_notified_when_a_new_post_is_published(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.create_unconfirmed_subscriber().await; app.create_confirmed_subscriber().await; app.admin_login().await; diff --git a/tests/api/subscriptions.rs b/tests/api/subscriptions.rs index 69153c0..f83bf2e 100644 --- a/tests/api/subscriptions.rs +++ b/tests/api/subscriptions.rs @@ -1,9 +1,10 @@ use crate::helpers::{TestApp, when_sending_an_email}; +use sqlx::PgPool; use wiremock::ResponseTemplate; -#[tokio::test] -async fn subscribe_displays_a_confirmation_message_for_valid_form_data() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn subscribe_displays_a_confirmation_message_for_valid_form_data(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; when_sending_an_email() .respond_with(ResponseTemplate::new(200)) @@ -19,9 +20,9 @@ async fn subscribe_displays_a_confirmation_message_for_valid_form_data() { assert!(html_fragment.contains("You'll receive a confirmation email shortly")); } -#[tokio::test] -async fn subscribe_persists_the_new_subscriber() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn subscribe_persists_the_new_subscriber(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; when_sending_an_email() .respond_with(ResponseTemplate::new(200)) @@ -45,9 +46,9 @@ async fn subscribe_persists_the_new_subscriber() { assert_eq!(saved.status, "pending_confirmation"); } -#[tokio::test] -async fn subscribe_returns_a_422_when_data_is_missing() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn subscribe_returns_a_422_when_data_is_missing(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let response = app.post_subscriptions(String::new()).await; @@ -58,9 +59,9 @@ async fn subscribe_returns_a_422_when_data_is_missing() { ); } -#[tokio::test] -async fn subscribe_sends_a_confirmation_email_for_valid_data() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn subscribe_sends_a_confirmation_email_for_valid_data(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let email = "alphonse.paix@outlook.com"; let body = format!("email={email}"); @@ -74,9 +75,9 @@ async fn subscribe_sends_a_confirmation_email_for_valid_data() { app.post_subscriptions(body).await; } -#[tokio::test] -async fn subscribe_sends_a_confirmation_email_with_a_link() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn subscribe_sends_a_confirmation_email_with_a_link(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let email = "alphonse.paix@outlook.com"; let body = format!("email={email}"); @@ -94,9 +95,9 @@ async fn subscribe_sends_a_confirmation_email_with_a_link() { assert_eq!(confirmation_links.html, confirmation_links.text); } -#[tokio::test] -async fn subscribe_fails_if_there_is_a_fatal_database_error() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn subscribe_fails_if_there_is_a_fatal_database_error(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let email = "alphonse.paix@outlook.com"; let body = format!("name=Alphonse&email={}", email); diff --git a/tests/api/subscriptions_confirm.rs b/tests/api/subscriptions_confirm.rs index 520bfc5..0af840d 100644 --- a/tests/api/subscriptions_confirm.rs +++ b/tests/api/subscriptions_confirm.rs @@ -1,9 +1,10 @@ use crate::helpers::{TestApp, when_sending_an_email}; +use sqlx::PgPool; use wiremock::ResponseTemplate; -#[tokio::test] -async fn confirmation_links_without_token_are_rejected_with_a_400() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn confirmation_links_without_token_are_rejected_with_a_400(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let response = reqwest::get(&format!("{}/subscriptions/confirm", &app.address)) .await @@ -11,9 +12,9 @@ async fn confirmation_links_without_token_are_rejected_with_a_400() { assert_eq!(400, response.status().as_u16()); } -#[tokio::test] -async fn clicking_on_the_link_shows_a_confirmation_message() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn clicking_on_the_link_shows_a_confirmation_message(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let email = "alphonse.paix@outlook.com"; let body = format!("email={email}"); @@ -39,9 +40,9 @@ async fn clicking_on_the_link_shows_a_confirmation_message() { ); } -#[tokio::test] -async fn clicking_on_the_confirmation_link_confirms_a_subscriber() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn clicking_on_the_confirmation_link_confirms_a_subscriber(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; let email = "alphonse.paix@outlook.com"; let body = format!("email={email}"); diff --git a/tests/api/unsubscribe.rs b/tests/api/unsubscribe.rs index c037dcd..1d2c52b 100644 --- a/tests/api/unsubscribe.rs +++ b/tests/api/unsubscribe.rs @@ -1,9 +1,10 @@ use crate::helpers::{TestApp, fake_newsletter_body, fake_post_body, when_sending_an_email}; +use sqlx::PgPool; use wiremock::ResponseTemplate; -#[tokio::test] -async fn subscriber_can_unsubscribe() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn subscriber_can_unsubscribe(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.create_confirmed_subscriber().await; app.admin_login().await; @@ -46,9 +47,9 @@ async fn subscriber_can_unsubscribe() { app.dispatch_all_pending_emails().await; } -#[tokio::test] -async fn a_valid_unsubscribe_link_is_present_in_new_post_email_notifications() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn a_valid_unsubscribe_link_is_present_in_new_post_email_notifications(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.create_confirmed_subscriber().await; app.admin_login().await; @@ -82,9 +83,9 @@ async fn a_valid_unsubscribe_link_is_present_in_new_post_email_notifications() { assert!(record.is_none()); } -#[tokio::test] -async fn a_valid_unsubscribe_link_is_present_in_standalone_emails() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn a_valid_unsubscribe_link_is_present_in_standalone_emails(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.create_confirmed_subscriber().await; app.admin_login().await; @@ -111,23 +112,19 @@ async fn a_valid_unsubscribe_link_is_present_in_standalone_emails() { .unwrap(); } -#[tokio::test] -async fn an_invalid_unsubscribe_token_is_rejected() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn an_invalid_unsubscribe_token_is_rejected(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.create_confirmed_subscriber().await; let response = app.get_unsubscribe_confirm("invalid-token").await; - // let response = reqwest::get(format!("{}/unsubscribe?token=invalid", app.address)) - // .await - // .unwrap(); - assert_eq!(response.status().as_u16(), 404); } -#[tokio::test] -async fn subscription_works_after_unsubscribe() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn subscription_works_after_unsubscribe(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.create_confirmed_subscriber().await; let record = sqlx::query!("SELECT email, unsubscribe_token FROM subscriptions") diff --git a/tests/api/unsubscribe_confirm.rs b/tests/api/unsubscribe_confirm.rs index fb3c7d4..1fc4c28 100644 --- a/tests/api/unsubscribe_confirm.rs +++ b/tests/api/unsubscribe_confirm.rs @@ -1,9 +1,10 @@ use crate::helpers::{TestApp, when_sending_an_email}; +use sqlx::PgPool; use wiremock::ResponseTemplate; -#[tokio::test] -async fn unsubscribe_form_sends_a_valid_link_if_email_is_in_database() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn unsubscribe_form_sends_a_valid_link_if_email_is_in_database(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.create_confirmed_subscriber().await; when_sending_an_email() @@ -48,9 +49,9 @@ async fn unsubscribe_form_sends_a_valid_link_if_email_is_in_database() { assert!(html_fragment.contains("Good bye, friend")); } -#[tokio::test] -async fn an_invalid_email_is_ignored() { - let app = TestApp::spawn().await; +#[sqlx::test] +async fn an_invalid_email_is_ignored(connection_pool: PgPool) { + let app = TestApp::spawn(connection_pool).await; app.create_confirmed_subscriber().await; when_sending_an_email()