diff --git a/src/email_client.rs b/src/email_client.rs index b340113..64305e6 100644 --- a/src/email_client.rs +++ b/src/email_client.rs @@ -35,7 +35,7 @@ impl EmailClient { text_content: &str, ) -> Result<(), reqwest::Error> { let url = format!("{}/email", self.base_url); - let builder = self + let _builder = self .http_client .post(&url) .header("X-Postmark-Server-Token", self.authorization_token.expose_secret()) diff --git a/tests/api/health_check.rs b/tests/api/health_check.rs new file mode 100644 index 0000000..4137fb0 --- /dev/null +++ b/tests/api/health_check.rs @@ -0,0 +1,16 @@ +use crate::helpers::spawn_app; + +#[tokio::test] +async fn health_check_works() { + let app = spawn_app().await; + let health_check_endpoint = format!("{}/health_check", app.address); + let client = reqwest::Client::new(); + let response = client + .get(health_check_endpoint) + .send() + .await + .expect("Failed to execute request."); + + assert!(response.status().is_success()); + assert_eq!(Some(0), response.content_length()); +} \ No newline at end of file diff --git a/tests/api/helpers.rs b/tests/api/helpers.rs new file mode 100644 index 0000000..f03831a --- /dev/null +++ b/tests/api/helpers.rs @@ -0,0 +1,88 @@ +use once_cell::sync::Lazy; +use secrecy::ExposeSecret; +use sqlx::{postgres::PgPoolOptions, Connection, Executor, PgConnection, Pool, Postgres}; +use testcontainers::{clients::Cli, RunnableImage}; +use uuid::Uuid; +use std:: net::TcpListener; +use zero2prod::{configuration::{get_configuration, DatabaseSettings}, email_client, telemetry::{get_subscriber, init_subscriber}}; + +pub struct TestApp { + pub address: String, + pub connection_pool: Pool, +} + +static TRACING: Lazy<()> = Lazy::new(|| { + let default_filter_level = "info".to_string(); + let subscriber_name = "test".to_string(); + + if std::env::var("TEST_LOG").is_ok() { + let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::stdout); + init_subscriber(subscriber); + } else { + let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::sink); + init_subscriber(subscriber); + } +}); + +fn create_db(config: &DatabaseSettings) -> RunnableImage { + RunnableImage::from(testcontainers_modules::postgres::Postgres::default()) + .with_env_var(("POSTGRES_PASSWORD", config.password.expose_secret())) + .with_env_var(("POSTGRES_USER", &config.username)) + .with_env_var(("POSTGRES_DB", &config.database_name)) +} + +pub async fn spawn_app() -> TestApp { + Lazy::force(&TRACING); + + let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port"); + let port = listener.local_addr().unwrap().port(); + let address = format!("http://127.0.0.1:{}", port); + + let mut config = get_configuration().expect("Failed to read configuration"); + let image = create_db(&config.database); + let docker = Cli::default(); + docker.run(image); + config.database.database_name = Uuid::new_v4().to_string(); + let connection_pool = configure_database(&config.database).await; + + let sender_email = config.email_client.sender().unwrap(); + let timeout = config.email_client.timeout(); + let email_client = email_client::EmailClient::new( + config.email_client.base_url, + sender_email, + config.email_client.authorization_token, + timeout, + ); + + let server = zero2prod::startup::run(listener, connection_pool.clone(), email_client).expect("Failed to bind address"); + let _fut = tokio::spawn(server); + + TestApp { + address, + connection_pool, + } + +} + +pub async fn configure_database(config: &DatabaseSettings) -> Pool { + 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_str()) + .await + .expect("Failed to create database."); + + let connection_pool = PgPoolOptions::new() + .max_connections(10) + .connect_with(config.with_db()) + .await + .expect("Failed to connect to Postgres."); + + sqlx::migrate!("./migrations") + .run(&connection_pool) + .await + .expect("Failed to run migrations."); + + connection_pool +} diff --git a/tests/api/main.rs b/tests/api/main.rs new file mode 100644 index 0000000..cb7c405 --- /dev/null +++ b/tests/api/main.rs @@ -0,0 +1,3 @@ +mod helpers; +mod health_check; +mod subscriptions; diff --git a/tests/api/subscriptions.rs b/tests/api/subscriptions.rs new file mode 100644 index 0000000..a825bae --- /dev/null +++ b/tests/api/subscriptions.rs @@ -0,0 +1,84 @@ +use crate::helpers::spawn_app; +use sqlx::query; + +#[tokio::test] +async fn subscribe_returns_a_200_for_valid_form_data() { + let app = spawn_app().await; + let client = reqwest::Client::new(); + + let body = "name=andre&email=andre.heber@gmx.net"; + let response = client + .post(&format!("{}/subscriptions", &app.address)) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(body) + .send() + .await + .expect("Failed to execute request."); + + assert_eq!(200, response.status().as_u16()); + + let saved = query!("SELECT email, name FROM subscriptions",) + .fetch_one(&app.connection_pool) + .await + .expect("Failed to fetch saved subscription."); + + assert_eq!(saved.email, "andre.heber@gmx.net"); + assert_eq!(saved.name, "andre"); +} + +#[tokio::test] +async fn subscribe_returns_a_400_when_data_is_missing() { + let app = spawn_app().await; + let client = reqwest::Client::new(); + let test_cases = vec![ + ("name=le%20guin", "missing the email"), + ("email=ursula_le_guin%40gmail.com", "missing the name"), + ("", "missing both name and email"), + ]; + + for (invalid_body, error_message) in test_cases { + let response = client + .post(&format!("{}/subscriptions", &app.address)) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(invalid_body) + .send() + .await + .expect("Failed to execute request."); + + assert_eq!( + 400, + response.status().as_u16(), + "The API did not fail with 400 Bad Request when the payload was {}.", + error_message + ); + } +} + +#[tokio::test] +async fn subscribe_returns_a_400_when_fields_are_present_but_invalid() { + let app = spawn_app().await; + let client = reqwest::Client::new(); + let test_cases = vec![ + ("name=&email=ursula_le_guin%40gmail.com", "name is empty"), + ("name=le%20guin&email=", "email is empty"), + ("name=&email=", "name and email are empty"), + ("name=Ursula&email=definitely-not-an-email", "invalid email"), + ]; + + for (invalid_body, error_message) in test_cases { + let response = client + .post(&format!("{}/subscriptions", &app.address)) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(invalid_body) + .send() + .await + .expect("Failed to execute request."); + + assert_eq!( + 400, + response.status().as_u16(), + "The API did not return a 400 Bad Request when the payload was {}.", + error_message + ); + } +} diff --git a/tests/health_check.rs b/tests/health_check.rs deleted file mode 100644 index 39a7c83..0000000 --- a/tests/health_check.rs +++ /dev/null @@ -1,184 +0,0 @@ -use once_cell::sync::Lazy; -use secrecy::ExposeSecret; -use sqlx::{postgres::PgPoolOptions, query, Connection, Executor, PgConnection, Pool, Postgres}; -use testcontainers::{clients::Cli, RunnableImage}; -use uuid::Uuid; -use std:: net::TcpListener; -use zero2prod::{configuration::{get_configuration, DatabaseSettings}, email_client, telemetry::{get_subscriber, init_subscriber}}; - -pub struct TestApp { - pub address: String, - pub connection_pool: Pool, -} - -static TRACING: Lazy<()> = Lazy::new(|| { - let default_filter_level = "info".to_string(); - let subscriber_name = "test".to_string(); - - if std::env::var("TEST_LOG").is_ok() { - let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::stdout); - init_subscriber(subscriber); - } else { - let subscriber = get_subscriber(subscriber_name, default_filter_level, std::io::sink); - init_subscriber(subscriber); - } -}); - -fn create_db(config: &DatabaseSettings) -> RunnableImage { - RunnableImage::from(testcontainers_modules::postgres::Postgres::default()) - .with_env_var(("POSTGRES_PASSWORD", config.password.expose_secret())) - .with_env_var(("POSTGRES_USER", &config.username)) - .with_env_var(("POSTGRES_DB", &config.database_name)) -} - -async fn spawn_app() -> TestApp { - Lazy::force(&TRACING); - - let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port"); - let port = listener.local_addr().unwrap().port(); - let address = format!("http://127.0.0.1:{}", port); - - let mut config = get_configuration().expect("Failed to read configuration"); - let image = create_db(&config.database); - let docker = Cli::default(); - docker.run(image); - config.database.database_name = Uuid::new_v4().to_string(); - let connection_pool = configure_database(&config.database).await; - - let sender_email = config.email_client.sender().unwrap(); - let timeout = config.email_client.timeout(); - let email_client = email_client::EmailClient::new( - config.email_client.base_url, - sender_email, - config.email_client.authorization_token, - timeout, - ); - - let server = zero2prod::startup::run(listener, connection_pool.clone(), email_client).expect("Failed to bind address"); - let _ = tokio::spawn(server); - - TestApp { - address, - connection_pool, - } -} - -pub async fn configure_database(config: &DatabaseSettings) -> Pool { - 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_str()) - .await - .expect("Failed to create database."); - - let connection_pool = PgPoolOptions::new() - .max_connections(10) - .connect_with(config.with_db()) - .await - .expect("Failed to connect to Postgres."); - - sqlx::migrate!("./migrations") - .run(&connection_pool) - .await - .expect("Failed to run migrations."); - - connection_pool -} - -#[tokio::test] -async fn health_check_works() { - let app = spawn_app().await; - let health_check_endpoint = format!("{}/health_check", app.address); - let client = reqwest::Client::new(); - let response = client - .get(health_check_endpoint) - .send() - .await - .expect("Failed to execute request."); - - assert!(response.status().is_success()); - assert_eq!(Some(0), response.content_length()); -} - -#[tokio::test] -async fn subscribe_returns_a_200_for_valid_form_data() { - let app = spawn_app().await; - let client = reqwest::Client::new(); - - let body = "name=andre&email=andre.heber@gmx.net"; - let response = client - .post(&format!("{}/subscriptions", &app.address)) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(body) - .send() - .await - .expect("Failed to execute request."); - - assert_eq!(200, response.status().as_u16()); - - let saved = query!("SELECT email, name FROM subscriptions",) - .fetch_one(&app.connection_pool) - .await - .expect("Failed to fetch saved subscription."); - - assert_eq!(saved.email, "andre.heber@gmx.net"); - assert_eq!(saved.name, "andre"); -} - -#[tokio::test] -async fn subscribe_returns_a_400_when_data_is_missing() { - let app = spawn_app().await; - let client = reqwest::Client::new(); - let test_cases = vec![ - ("name=le%20guin", "missing the email"), - ("email=ursula_le_guin%40gmail.com", "missing the name"), - ("", "missing both name and email"), - ]; - - for (invalid_body, error_message) in test_cases { - let response = client - .post(&format!("{}/subscriptions", &app.address)) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(invalid_body) - .send() - .await - .expect("Failed to execute request."); - - assert_eq!( - 400, - response.status().as_u16(), - "The API did not fail with 400 Bad Request when the payload was {}.", - error_message - ); - } -} - -#[tokio::test] -async fn subscribe_returns_a_400_when_fields_are_present_but_invalid() { - let app = spawn_app().await; - let client = reqwest::Client::new(); - let test_cases = vec![ - ("name=&email=ursula_le_guin%40gmail.com", "name is empty"), - ("name=le%20guin&email=", "email is empty"), - ("name=&email=", "name and email are empty"), - ("name=Ursula&email=definitely-not-an-email", "invalid email"), - ]; - - for (invalid_body, error_message) in test_cases { - let response = client - .post(&format!("{}/subscriptions", &app.address)) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(invalid_body) - .send() - .await - .expect("Failed to execute request."); - - assert_eq!( - 400, - response.status().as_u16(), - "The API did not return a 400 Bad Request when the payload was {}.", - error_message - ); - } -}