diff --git a/Cargo.toml b/Cargo.toml index 3b53f45..097b8f5 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.11" +uuid = {version = "0.8.1", features = ["v4"]} +chrono = "0.4.15" [dependencies.sqlx] version = "0.5.7" diff --git a/src/configuration.rs b/src/configuration.rs index 7999632..1a5bc39 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,7 +1,7 @@ #[derive(serde::Deserialize)] pub struct Settings { - pub database: DatabaseSettings, - pub application_port: u16 + pub database: DatabaseSettings, + pub application_port: u16, } #[derive(serde::Deserialize)] @@ -10,7 +10,7 @@ pub struct DatabaseSettings { pub password: String, pub port: u16, pub host: String, - pub database_name: String + pub database_name: String, } pub fn get_configuration() -> Result { @@ -31,4 +31,11 @@ impl DatabaseSettings { self.username, self.password, self.host, self.port, self.database_name ) } -} \ No newline at end of file + + pub fn connection_string_without_db(&self) -> String { + format!( + "postgres://{}:{}@{}:{}", + self.username, self.password, self.host, self.port + ) + } +} diff --git a/src/lib.rs b/src/lib.rs index 16870f5..5d1dce7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,3 @@ pub mod configuration; pub mod routes; -pub mod startup; \ No newline at end of file +pub mod startup; diff --git a/src/main.rs b/src/main.rs index fefbab3..20aa64a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,16 @@ -use std::net::TcpListener; -use newsletter::startup::run; use newsletter::configuration::get_configuration; +use newsletter::startup::run; +use sqlx::PgPool; +use std::net::TcpListener; #[tokio::main] async fn main() -> std::io::Result<()> { // Panic if we can't read configuration let configuration = get_configuration().expect("Failed to read configuration."); + let connection_pool = PgPool::connect(&configuration.database.connection_string()) + .await + .expect("Failed to connect to Postgres!"); let address = format!("127.0.0.1:{}", configuration.application_port); let listener = TcpListener::bind(address)?; - run(listener)?.await -} \ No newline at end of file + run(listener, connection_pool)?.await +} diff --git a/src/routes/health_check.rs b/src/routes/health_check.rs index b74f064..d7eb4e0 100644 --- a/src/routes/health_check.rs +++ b/src/routes/health_check.rs @@ -1,5 +1,5 @@ use actix_web::HttpResponse; pub async fn health_check() -> HttpResponse { - HttpResponse::Ok().finish() -} \ No newline at end of file + HttpResponse::Ok().finish() +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 1a267f9..90ffeed 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -2,4 +2,4 @@ mod health_check; mod subscriptions; pub use health_check::*; -pub use subscriptions::*; \ No newline at end of file +pub use subscriptions::*; diff --git a/src/routes/subscriptions.rs b/src/routes/subscriptions.rs index f2ffee6..6433376 100644 --- a/src/routes/subscriptions.rs +++ b/src/routes/subscriptions.rs @@ -1,11 +1,32 @@ use actix_web::{web, HttpResponse}; +use chrono::Utc; +use sqlx::PgPool; +use uuid::Uuid; #[derive(serde::Deserialize)] pub struct FormData { email: String, - name: String + name: String, } -pub async fn subscribe(_form: web::Form) -> HttpResponse { - HttpResponse::Ok().finish() -} \ No newline at end of file +pub async fn subscribe(form: web::Form, pool: web::Data) -> HttpResponse { + 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(pool.get_ref()) + .await + { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => { + println!("Failed to execute the query: {}", e); + HttpResponse::InternalServerError().finish() + } + } +} diff --git a/src/startup.rs b/src/startup.rs index cd6340c..305f82c 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -1,16 +1,19 @@ use actix_web::dev::Server; use actix_web::{web, App, HttpServer}; +use sqlx::PgPool; use std::net::TcpListener; use crate::routes::{health_check, subscribe}; -pub fn run(listener: TcpListener) -> Result { - let server = HttpServer::new(|| { - App::new() - .route("/health_check", web::get().to(health_check)) - .route("/subscriptions", web::post().to(subscribe)) - }) - .listen(listener)? - .run(); - Ok(server) -} \ No newline at end of file +pub fn run(listener: TcpListener, db_pool: PgPool) -> Result { + let db_pool = web::Data::new(db_pool); + let server = HttpServer::new(move || { + App::new() + .route("/health_check", web::get().to(health_check)) + .route("/subscriptions", web::post().to(subscribe)) + .app_data(db_pool.clone()) + }) + .listen(listener)? + .run(); + Ok(server) +} diff --git a/tests/health_check.rs b/tests/health_check.rs index 533d89a..353f598 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -1,28 +1,67 @@ -use std::net::TcpListener; +use newsletter::configuration::{get_configuration, DatabaseSettings}; use newsletter::startup::run; -use sqlx::{PgConnection, Connection}; -use newsletter::configuration::get_configuration; +use sqlx::{PgPool, Connection, Executor, PgConnection}; +use std::net::TcpListener; +use uuid::Uuid; -fn spawn_app() -> String { +pub struct TestApp { + pub address: String, + pub db_pool: PgPool +} + +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 server = run(listener).expect("Failed to bind address"); + let address = format!("http://127.0.0.1:{}", port); + + let mut configuration = get_configuration().expect("Failed to read configuration."); + configuration.database.database_name = Uuid::new_v4().to_string(); + let connection_pool = configure_database(&configuration.database).await; + + let server = run(listener, connection_pool.clone()) + .expect("Failed to bind address"); let _ = tokio::spawn(server); - format!("http://127.0.0.1:{}", port) + + TestApp { + address, + db_pool: connection_pool + } } +pub async fn configure_database(config: &DatabaseSettings) -> PgPool { + // Create Database + let mut connection = PgConnection::connect(&config.connection_string_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"); + + // Migrate Database + let connection_pool = PgPool::connect(&config.connection_string()) + .await + .expect("Failed to connect to Postgres!"); + + sqlx::migrate!("./migrations") + .run(&connection_pool) + .await + .expect("Failed to migrate the database"); + + connection_pool +} #[tokio::test] async fn health_check_works() { // Arrange - let address = spawn_app(); - // to perform HTTP requests against our application. + let app = spawn_app().await; + // to perform HTTP requests against our application. let client = reqwest::Client::new(); - + // Act let response = client - .get(&format!("{}/health_check", address)) + .get(&format!("{}/health_check", app.address)) .send() .await .expect("Failed to execute request."); @@ -34,17 +73,12 @@ async fn health_check_works() { #[tokio::test] async fn subscribe_returns_200_for_valid_form_data() { - let app_address = spawn_app(); - let configuration = get_configuration().expect("Failed to read configuration"); - let connection_string = configuration.database.connection_string(); - let mut connection = PgConnection::connect(&connection_string) - .await - .expect("Failed to connect to Postgres"); + let app = spawn_app().await; let client = reqwest::Client::new(); let body = "name=dibakar%20dhar&email=where_is_dibakar%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() @@ -54,28 +88,27 @@ async fn subscribe_returns_200_for_valid_form_data() { assert_eq!(200, response.status().as_u16()); let saved = sqlx::query!("SELECT email, name FROM subscriptions",) - .fetch_one(&mut connection) + .fetch_one(&app.db_pool) .await .expect("Failed to fetch saved subscription"); assert_eq!(saved.email, "where_is_dibakar@gmail.com"); assert_eq!(saved.name, "dibakar dhar"); - } #[tokio::test] async fn subscribe_returns_400_when_data_is_missing() { - let app_address = spawn_app(); + let app = spawn_app().await; let client = reqwest::Client::new(); let test_cases = vec![ ("name=le%20guin", "missing the email"), ("email=uresela_le_guin%40gmail.com", "missing the name"), - ("", "missing both name and email") + ("", "missing both name and email"), ]; 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() @@ -85,7 +118,8 @@ async fn subscribe_returns_400_when_data_is_missing() { assert_eq!( 400, response.status().as_u16(), - "The API did not fail with 400 Bad Request when the payload was {}.", error_message + "The API did not fail with 400 Bad Request when the payload was {}.", + error_message ); } -} \ No newline at end of file +}