diff --git a/.env b/.env index 88cfb53..fd2d8a6 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -DATABASE_URL="postgres://postgres:password@localhost:5432/newsletter" +DATABASE_URL=postgres://postgres:password@localhost:5432/newsletter diff --git a/Cargo.lock b/Cargo.lock index 7ba316c..7a96cd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,7 +221,10 @@ checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", "windows-targets 0.52.4", ] @@ -2293,6 +2296,10 @@ name = "uuid" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom", + "serde", +] [[package]] name = "vcpkg" @@ -2593,11 +2600,13 @@ name = "zero2prod" version = "0.1.0" dependencies = [ "axum", + "chrono", "config", "reqwest", "serde", "sqlx", "tokio", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5f2af9e..27fc567 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,20 @@ name = "zero2prod" [dependencies] axum = "0.7" +chrono = { version = "0.4", features = ["serde", "clock"] } config = "0.14" serde = { version = "1", features = ["derive"] } -sqlx = { version = "0.7", features = ["runtime-tokio", "tls-rustls", "macros", "postgres", "uuid", "chrono", "migrate"] } +sqlx = { version = "0.7", features = [ + "runtime-tokio", + "tls-rustls", + "macros", + "postgres", + "uuid", + "chrono", + "migrate", +] } tokio = { version = "1", features = ["full"] } +uuid = { version = "1.8", features = ["v4", "serde"] } [dev-dependencies] reqwest = "0.12" diff --git a/src/configuation.rs b/src/configuation.rs index 7c3e4d4..660f9af 100644 --- a/src/configuation.rs +++ b/src/configuation.rs @@ -17,6 +17,15 @@ pub struct DatabaseSettings { pub database_name: String, } +impl DatabaseSettings { + pub fn to_string_no_db(&self) -> String { + format!( + "postgres://{}:{}@{}:{}", + self.username, self.password, self.host, self.port + ) + } +} + impl Display for DatabaseSettings { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( diff --git a/src/lib.rs b/src/lib.rs index e511de0..7d0609b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,2 @@ pub mod configuation; pub mod routes; -pub mod startup; diff --git a/src/main.rs b/src/main.rs index bff2e23..e78eff0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,19 @@ use std::net::SocketAddr; +use sqlx::postgres::PgPoolOptions; use tokio::net::TcpListener; use zero2prod::{configuation::get_configuration, routes::route}; #[tokio::main] async fn main() -> Result<(), std::io::Error> { let configuation = get_configuration().expect("Failed to read configuation."); + let pool = PgPoolOptions::new() + .connect(&configuation.database.to_string()) + .await + .expect("Failed to connect to Postgres."); let addr = SocketAddr::from(([127, 0, 0, 1], configuation.application_port)); let listener = TcpListener::bind(addr) .await .expect("Failed to bind random port"); - axum::serve(listener, route()).await + axum::serve(listener, route(pool)).await } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index ac730d7..bf72b13 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,15 +1,18 @@ mod health_check; mod subscibtions; + use axum::{ routing::{get, post}, Router, }; pub use health_check::*; +use sqlx::PgPool; pub use subscibtions::*; -pub fn route() -> Router { +pub fn route(state: PgPool) -> Router { Router::new() .route("/health_check", get(health_check)) .route("/subscribtions", post(subscribe)) + .with_state(state) } diff --git a/src/routes/subscibtions.rs b/src/routes/subscibtions.rs index d437f10..603a520 100644 --- a/src/routes/subscibtions.rs +++ b/src/routes/subscibtions.rs @@ -1,11 +1,36 @@ -use axum::{http::StatusCode, response::IntoResponse, Form}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Form}; +use chrono::Utc; use serde::Deserialize; +use sqlx::PgPool; +use uuid::Uuid; + #[derive(Deserialize)] pub struct FormData { name: String, email: String, } -pub async fn subscribe(Form(form): Form) -> impl IntoResponse { - StatusCode::OK +pub async fn subscribe( + State(conn): State, + Form(form): Form, +) -> impl IntoResponse { + match sqlx::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(&conn) + .await + { + Ok(_) => StatusCode::OK, + Err(e) => { + println!("Failed to execute query: {:?}", e); + StatusCode::INTERNAL_SERVER_ERROR + } + } } diff --git a/src/startup.rs b/src/startup.rs deleted file mode 100644 index 139597f..0000000 --- a/src/startup.rs +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/tests/health_check.rs b/tests/health_check.rs index f5e813a..d28e3e3 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -1,12 +1,16 @@ use reqwest::Client; -use sqlx::{Connection, PgConnection}; +use sqlx::{postgres::PgPoolOptions, Connection, Executor, PgConnection, PgPool}; use tokio::net::TcpListener; -use zero2prod::{configuation::get_configuration, routes::route}; +use uuid::Uuid; +use zero2prod::{ + configuation::{get_configuration, DatabaseSettings}, + routes::route, +}; #[tokio::test] async fn health_check() { - let address = spawn_app().await; - let url = format!("{}/health_check", &address); + let app = spawn_app().await; + let url = format!("{}/health_check", &app.address); let client = Client::new(); let response = client .get(&url) @@ -20,7 +24,7 @@ async fn health_check() { #[tokio::test] async fn subscribe_returns_200_for_valid_form_data() { - let address = spawn_app().await; + let app = spawn_app().await; let configuration = get_configuration().expect("Failed to read configuration."); let db_url = configuration.database.to_string(); let mut connection = PgConnection::connect(&db_url) @@ -30,7 +34,7 @@ async fn subscribe_returns_200_for_valid_form_data() { let body = "name=Kristofers%20Solo&email=dev%40kristofers.solo"; let response = client - .post(&format!("{}/subscribtions", &address)) + .post(&format!("{}/subscribtions", &app.address)) .header("Content-Type", "application/x-www-form-urlencoded") .body(body) .send() @@ -42,7 +46,7 @@ async fn subscribe_returns_200_for_valid_form_data() { r#" SELECT email, name FROM subscriptions - "# + "# ) .fetch_one(&mut connection) .await @@ -54,7 +58,7 @@ async fn subscribe_returns_200_for_valid_form_data() { #[tokio::test] async fn subscribe_returns_400_when_data_is_missing() { - let address = spawn_app().await; + let app = spawn_app().await; let client = Client::new(); let test_cases = vec![ @@ -65,7 +69,7 @@ async fn subscribe_returns_400_when_data_is_missing() { for (invalid_body, error_message) in test_cases { let response = client - .post(&format!("{}/subscribtions", &address)) + .post(&format!("{}/subscribtions", &app.address)) .header("Content-Type", "application/x-www-form-urlencoded") .body(invalid_body) .send() @@ -81,11 +85,57 @@ async fn subscribe_returns_400_when_data_is_missing() { } } -async fn spawn_app() -> String { +async fn spawn_app() -> TestApp { let listener = TcpListener::bind("127.0.0.1:0") .await .expect("Failed to bind random port"); let port = listener.local_addr().unwrap().port(); - let _ = tokio::spawn(async move { axum::serve(listener, route()).await.unwrap() }); - format!("http://127.0.0.1:{}", port) + let address = format!("http://127.0.0.1:{}", port); + let mut config = get_configuration().expect("Failed to read configuration."); + + config.database.database_name = Uuid::new_v4().to_string(); + let pool = configure_database(&config.database).await; + let pool_clone = pool.clone(); + let _ = tokio::spawn(async move { + axum::serve(listener, route(pool_clone)) + .await + .expect("Failed to bind address.") + }); + TestApp { address, pool } +} + +async fn configure_database(config: &DatabaseSettings) -> PgPool { + let mut connection = PgConnection::connect(&config.to_string_no_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 pool = PgPoolOptions::new() + .connect(&config.to_string()) + .await + .expect("Failed to connect to Postgres."); + + sqlx::migrate!("./migrations") + .run(&pool) + .await + .expect("Failed to migrate database"); + + pool +} + +pub struct TestApp { + pub address: String, + pub pool: PgPool, }