Update test suite to drop database automatically when test is successfull
Some checks failed
Rust / Test (push) Has been cancelled
Rust / Rustfmt (push) Has been cancelled
Rust / Clippy (push) Has been cancelled
Rust / Code coverage (push) Has been cancelled

This commit is contained in:
Alphonse Paix
2025-09-24 02:55:18 +02:00
parent 9ea539e5cc
commit 33281132c6
12 changed files with 128 additions and 148 deletions

View File

@@ -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());

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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}");

View File

@@ -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")

View File

@@ -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()