From a0aa12872dc2bcad85b2864f858d9b58ea919f46 Mon Sep 17 00:00:00 2001 From: Andre Heber Date: Thu, 15 Feb 2024 20:14:27 +0100 Subject: [PATCH] insert subscription, db test isolation --- Cargo.lock | 7 ++++ Cargo.toml | 2 + src/configuration.rs | 14 ++++--- src/main.rs | 4 +- src/routes/subscriptions.rs | 24 +++++++++++- src/startup.rs | 6 +-- tests/health_check.rs | 74 +++++++++++++++++++++++-------------- 7 files changed, 90 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bac433c..d20a2f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,7 +413,9 @@ checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "wasm-bindgen", "windows-targets 0.52.0", ] @@ -2563,6 +2565,9 @@ name = "uuid" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +dependencies = [ + "getrandom", +] [[package]] name = "vcpkg" @@ -2853,11 +2858,13 @@ name = "zero2prod" version = "0.1.0" dependencies = [ "actix-web", + "chrono", "config", "reqwest", "serde", "sqlx", "tokio", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 6593635..a2012c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ tokio = { version = "1", features = ["macros", "rt-multi-thread"] } reqwest = "0.11" serde = { version = "1", features = ["derive"] } config = "0.14" +uuid = { version = "1.7.0", features = ["v4"] } +chrono = "0.4.34" [dependencies.sqlx] version = "0.7.3" diff --git a/src/configuration.rs b/src/configuration.rs index ffa3ce0..6a15c3a 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -22,17 +22,19 @@ impl DatabaseSettings { self.username, self.password, self.host, self.port, self.database_name ) } + + pub fn connection_string_without_db(&self) -> String { + format!( + "postgres://{}:{}@{}:{}", + self.username, self.password, self.host, self.port + ) + } } pub fn get_configuration() -> Result { - // let mut settings = config::Config::default(); - - // settings.merge(config::File::with_name("configuration"))?; - - // settings.try_into() let settings = Config::builder() .add_source(config::File::with_name("configuration")) .build()?; - Ok(settings.try_deserialize()?) + settings.try_deserialize() } diff --git a/src/main.rs b/src/main.rs index 203f6bb..5efee55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,10 +6,10 @@ use zero2prod::startup::run; #[tokio::main] async fn main() -> std::io::Result<()> { let config = get_configuration().expect("Failed to read configuration"); - let connection = PgPoolOptions::new() + let connection_pool = PgPoolOptions::new() .max_connections(10).connect(&config.database.connection_string()).await.expect("Failed to connect to Postgres."); let address = format!("127.0.0.1:{}", config.application_port); let listener = TcpListener::bind(address).expect("Failed to bind random port"); - run(listener, connection)?.await + run(listener, connection_pool)?.await } diff --git a/src/routes/subscriptions.rs b/src/routes/subscriptions.rs index 9650074..df93978 100644 --- a/src/routes/subscriptions.rs +++ b/src/routes/subscriptions.rs @@ -1,5 +1,8 @@ use actix_web::{web, HttpResponse}; use serde::Deserialize; +use chrono::Utc; +use uuid::Uuid; +use sqlx::{query, Pool, Postgres}; #[derive(Deserialize)] pub struct FormData { @@ -7,6 +10,23 @@ pub struct FormData { pub name: String, } -pub async fn subscribe(_form: web::Form) -> HttpResponse { - HttpResponse::Ok().finish() +pub async fn subscribe(form: web::Form, connection_pool: web::Data>) -> HttpResponse { + match query!( + r#" + INSERT INTO subscriptions (id, email, name, subscribed_at) + VALUES ($1, $2, $3, $4) + "#, + Uuid::new_v4(), + form.email, + form.name, + Utc::now() + ) + .execute(connection_pool.get_ref()) + .await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => { + println!("Failed to execute query: {:?}", e); + HttpResponse::InternalServerError().finish() + } + } } diff --git a/src/startup.rs b/src/startup.rs index 7c09d17..3308a70 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -5,13 +5,13 @@ use std::net::TcpListener; use crate::routes::{health_check, subscribe}; -pub fn run(listener: TcpListener, connection: Pool) -> Result { - let connection = web::Data::new(connection); +pub fn run(listener: TcpListener, connection_pool: Pool) -> Result { + let connection_pool = web::Data::new(connection_pool); let server = HttpServer::new(move || { App::new() .route("/health_check", web::get().to(health_check)) .route("/subscriptions", web::post().to(subscribe)) - .app_data(connection.clone()) + .app_data(connection_pool.clone()) }) .listen(listener)? .run(); diff --git a/tests/health_check.rs b/tests/health_check.rs index 93cbe73..30c423d 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -1,26 +1,55 @@ -use sqlx::{postgres::PgPoolOptions, query, Connection, PgConnection, Pool, Postgres}; +use sqlx::{postgres::PgPoolOptions, query, Executor, Pool, Postgres}; +use uuid::Uuid; use std::net::TcpListener; -use zero2prod::configuration::get_configuration; +use zero2prod::configuration::{get_configuration, DatabaseSettings}; -fn spawn_app(connection: Pool) -> String { +pub struct TestApp { + pub address: String, + pub connection_pool: Pool, +} + +async fn spawn_app() -> TestApp { 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 server = zero2prod::startup::run(listener, connection).expect("Failed to bind address"); - tokio::spawn(server); + let mut config = get_configuration().expect("Failed to read configuration"); + config.database.database_name = Uuid::new_v4().to_string(); + let connection_pool = configure_database(&config.database).await; - format!("http://127.0.0.1:{}", port) + let server = zero2prod::startup::run(listener, connection_pool.clone()).expect("Failed to bind address"); + let _ = tokio::spawn(server); + + TestApp { + address, + connection_pool, + } +} + +pub async fn configure_database(config: &DatabaseSettings) -> Pool { + let connection_pool = PgPoolOptions::new() + .max_connections(10) + .connect(&config.connection_string_without_db()) + .await + .expect("Failed to connect to Postgres."); + + connection_pool.execute(format!(r#"CREATE DATABASE "{}";"#, config.database_name).as_str()) + .await + .expect("Failed to create database."); + + sqlx::migrate!("./migrations") + .run(&connection_pool) + .await + .expect("Failed to run migrations."); + + connection_pool } #[tokio::test] async fn health_check_works() { - let config = get_configuration().expect("Failed to read configuration"); - let connection = PgPoolOptions::new() - .max_connections(10).connect(&config.database.connection_string()).await.expect("Failed to connect to Postgres."); - - let address = spawn_app(connection); - let health_check_endpoint = format!("{}/health_check", address); + 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) @@ -34,19 +63,12 @@ async fn health_check_works() { #[tokio::test] async fn subscribe_returns_a_200_for_valid_form_data() { - let config = get_configuration().expect("Failed to read configuration"); - let connection = PgPoolOptions::new() - .max_connections(10).connect(&config.database.connection_string()).await.expect("Failed to connect to Postgres."); - - let app_address = spawn_app(connection); - - let mut connection = PgConnection::connect(&config.database.connection_string()).await.expect("Failed to connect to Postgres."); - + let app = spawn_app().await; let client = reqwest::Client::new(); let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; let response = client - .post(&format!("{}/subscriptions", &app_address)) + .post(&format!("{}/subscriptions", &app.address)) .header("Content-Type", "application/x-www-form-urlencoded") .body(body) .send() @@ -56,7 +78,7 @@ async fn subscribe_returns_a_200_for_valid_form_data() { assert_eq!(200, response.status().as_u16()); let saved = query!("SELECT email, name FROM subscriptions",) - .fetch_one(&mut connection) + .fetch_one(&app.connection_pool) .await .expect("Failed to fetch saved subscription."); @@ -66,11 +88,7 @@ async fn subscribe_returns_a_200_for_valid_form_data() { #[tokio::test] async fn subscribe_returns_a_400_when_data_is_missing() { - let config = get_configuration().expect("Failed to read configuration"); - let connection = PgPoolOptions::new() - .max_connections(10).connect(&config.database.connection_string()).await.expect("Failed to connect to Postgres."); - - let app_address = spawn_app(connection); + let app = spawn_app().await; let client = reqwest::Client::new(); let test_cases = vec![ ("name=le%20guin", "missing the email"), @@ -80,7 +98,7 @@ async fn subscribe_returns_a_400_when_data_is_missing() { for (invalid_body, error_message) in test_cases { let response = client - .post(&format!("{}/subscriptions", &app_address)) + .post(&format!("{}/subscriptions", &app.address)) .header("Content-Type", "application/x-www-form-urlencoded") .body(invalid_body) .send()