diff --git a/Cargo.lock b/Cargo.lock index 550e434..8b9efd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -245,6 +245,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -273,9 +279,9 @@ dependencies = [ [[package]] name = "atoi" -version = "0.4.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" dependencies = [ "num-traits", ] @@ -417,15 +423,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" dependencies = [ "async-trait", - "json5", "lazy_static", "nom", "pathdiff", - "ron", - "rust-ini", "serde", - "serde_json", - "toml", "yaml-rust", ] @@ -473,18 +474,18 @@ dependencies = [ [[package]] name = "crc" -version = "2.1.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" dependencies = [ "crc-catalog", ] [[package]] name = "crc-catalog" -version = "1.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" [[package]] name = "crc32fast" @@ -575,16 +576,10 @@ dependencies = [ ] [[package]] -name = "dlv-list" -version = "0.3.0" +name = "dotenvy" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" - -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" @@ -774,29 +769,27 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash 0.7.6", -] +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ - "ahash 0.7.6", + "ahash 0.8.3", + "allocator-api2", ] [[package]] name = "hashlink" -version = "0.7.0" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" dependencies = [ - "hashbrown 0.11.2", + "hashbrown 0.14.0", ] [[package]] @@ -1000,17 +993,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "language-tags" version = "0.3.2" @@ -1229,16 +1211,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "ordered-multimap" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" -dependencies = [ - "dlv-list", - "hashbrown 0.12.3", -] - [[package]] name = "parking_lot" version = "0.11.2" @@ -1305,50 +1277,6 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" -[[package]] -name = "pest" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1acb4a4365a13f749a93f1a094a7805e5cfa0955373a9de860d962eaa3a5fe5a" -dependencies = [ - "thiserror", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666d00490d4ac815001da55838c500eafb0320019bbaa44444137c48b443a853" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ca01446f50dbda87c1786af8770d535423fa8a53aec03b8f4e3d7eb10e0929" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.29", -] - -[[package]] -name = "pest_meta" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56af0a30af74d0445c0bf6d9d051c979b516a1a5af790d251daee76005420a48" -dependencies = [ - "once_cell", - "pest", - "sha2", -] - [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1531,27 +1459,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "ron" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" -dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", - "serde", -] - -[[package]] -name = "rust-ini" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" -dependencies = [ - "cfg-if", - "ordered-multimap", -] - [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1582,17 +1489,25 @@ dependencies = [ [[package]] name = "rustls" -version = "0.19.1" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ - "base64 0.13.1", "log", "ring", "sct", "webpki", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.3", +] + [[package]] name = "ryu" version = "1.0.15" @@ -1616,9 +1531,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ "ring", "untrusted", @@ -1696,17 +1611,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha-1" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "sha1" version = "0.10.5" @@ -1781,9 +1685,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "sqlformat" -version = "0.1.8" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4" +checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" dependencies = [ "itertools", "nom", @@ -1792,9 +1696,9 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.5.13" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551873805652ba0d912fec5bbb0f8b4cdd96baf8e2ebf5970e5671092966019b" +checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" dependencies = [ "sqlx-core", "sqlx-macros", @@ -1802,9 +1706,9 @@ dependencies = [ [[package]] name = "sqlx-core" -version = "0.5.13" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48c61941ccf5ddcada342cd59e3e5173b007c509e1e8e990dafc830294d9dc5" +checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" dependencies = [ "ahash 0.7.6", "atoi", @@ -1816,6 +1720,7 @@ dependencies = [ "crc", "crossbeam-queue", "dirs", + "dotenvy", "either", "event-listener", "futures-channel", @@ -1837,9 +1742,10 @@ dependencies = [ "percent-encoding", "rand", "rustls", + "rustls-pemfile", "serde", "serde_json", - "sha-1", + "sha1", "sha2", "smallvec", "sqlformat", @@ -1849,18 +1755,17 @@ dependencies = [ "tokio-stream", "url", "uuid", - "webpki", "webpki-roots", "whoami", ] [[package]] name = "sqlx-macros" -version = "0.5.13" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0fba2b0cae21fc00fe6046f8baa4c7fcb49e379f0f592b04696607f69ed2e1" +checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" dependencies = [ - "dotenv", + "dotenvy", "either", "heck", "once_cell", @@ -1875,11 +1780,10 @@ dependencies = [ [[package]] name = "sqlx-rt" -version = "0.5.13" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4db708cd3e459078f85f39f96a00960bd841f66ee2a669e90bf36907f5a79aae" +checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" dependencies = [ - "actix-rt", "once_cell", "tokio", "tokio-rustls", @@ -2041,9 +1945,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.22.0" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ "rustls", "tokio", @@ -2075,15 +1979,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "tower-service" version = "0.3.2" @@ -2123,12 +2018,6 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" -[[package]] -name = "ucd-trie" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" - [[package]] name = "unicode-bidi" version = "0.3.13" @@ -2181,9 +2070,12 @@ dependencies = [ [[package]] name = "uuid" -version = "0.8.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom", +] [[package]] name = "vcpkg" @@ -2290,9 +2182,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.21.4" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" dependencies = [ "ring", "untrusted", @@ -2300,9 +2192,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.21.1" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" dependencies = [ "webpki", ] @@ -2438,11 +2330,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 e0682ca..01ec5e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,29 +5,21 @@ version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [lib] -poath = "src/lib.rs" - -[dependencies] -actix-web = "4.3.1" -config = "0.13.3" -reqwest = "0.11.20" -serde = { version = "1.0.188", features = ["derive"] } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } - -[dependencies.sqlx] -version = "0.5.7" -default-features = false -features = [ - "runtime-actix-rustls", - "macros", - "postgres", - "uuid", - "chrono", - "migrate" -] +path = "src/lib.rs" [[bin]] path = "src/main.rs" name = "zero2prod" + +[dependencies] +actix-web = "4" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +serde = "1.0.115" +config = { version = "0.13", default-features = false, features = ["yaml"] } +sqlx = { version = "0.6", default-features = false, features = ["runtime-actix-rustls", "macros", "postgres", "uuid", "chrono", "migrate"] } +uuid = { version = "1", features = ["v4"] } +chrono = { version = "0.4.22", default-features = false, features = ["clock"] } + +[dev-dependencies] +reqwest = { version = "0.11", features = ["json"] } diff --git a/src/configurations.rs b/src/configurations.rs index f88f564..e811b4a 100644 --- a/src/configurations.rs +++ b/src/configurations.rs @@ -20,6 +20,13 @@ impl DatabaseSettings { self.username, self.password, self.host, self.port, self.database_name ) } + + pub fn get_connnection_string_without_db(&self) -> String { + format!( + "postgres://{}:{}@{}:{}", + self.username, self.password, self.host, self.port + ) + } } pub fn get_configuration() -> Result { diff --git a/src/main.rs b/src/main.rs index cc046d6..1d68d70 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use sqlx::PgPool; use std::net::TcpListener; use zero2prod::{configurations::get_configuration, startup::run}; @@ -5,8 +6,11 @@ use zero2prod::{configurations::get_configuration, startup::run}; async fn main() -> Result<(), std::io::Error> { // We want to panic if we cannot read the configuration let configuration = get_configuration().expect("Failed to read configurations"); + let connection = PgPool::connect(&configuration.database.get_connnection_string()) + .await + .expect("Failed to connect to Postgresf"); // Bind the TCP listener socket address with the configuration port let address = format!("127.0.0.1:{}", configuration.application_port); let listener = TcpListener::bind(address).expect("Failed to bind random port"); - run(listener)?.await + run(listener, connection)?.await } diff --git a/src/routes/subscriptions.rs b/src/routes/subscriptions.rs index 371a75c..7dfa4e2 100644 --- a/src/routes/subscriptions.rs +++ b/src/routes/subscriptions.rs @@ -1,4 +1,7 @@ use actix_web::{web, HttpResponse}; +use chrono::Utc; +use sqlx::PgPool; +use uuid::Uuid; #[allow(dead_code)] #[derive(serde::Deserialize)] @@ -7,6 +10,26 @@ pub struct FormData { name: String, } -pub async fn subscribe(_form: web::Form) -> HttpResponse { - HttpResponse::Ok().finish() +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() + ) + // We use `get_ref` to get an immutable reference to the `PgConnection` + // wrapped by `web::Data`. + .execute(pool.get_ref()) + .await + { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => { + eprintln!("Failed to execute query: {}", e); + HttpResponse::InternalServerError().finish() + } + } } diff --git a/src/startup.rs b/src/startup.rs index 1f65c5a..1273de5 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -1,14 +1,17 @@ use std::net::TcpListener; -use actix_web::{dev::Server, HttpServer, App, web}; +use actix_web::{dev::Server, web, App, HttpServer}; +use sqlx::PgPool; use crate::routes::{health_check, subscribe}; -pub fn run(listener: TcpListener) -> Result { - let server = HttpServer::new(|| { +pub fn run(listener: TcpListener, db_pool: PgPool) -> Result { + let connection = 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(connection.clone()) }) .listen(listener)? .run(); diff --git a/tests/health_check.rs b/tests/health_check.rs index c8b4fd5..f663cc6 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -1,20 +1,63 @@ +use actix_web::dev::ConnectionInfo; +use sqlx::{Connection, Executor, PgConnection, PgPool}; use std::net::TcpListener; - -use sqlx::Connection; +use uuid::Uuid; use zero2prod::configurations::get_configuration; -fn spawn_app() -> String { +pub struct TestApp { + pub address: String, + pub db_pool: sqlx::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 = zero2prod::startup::run(listener).expect("Failed to bind address"); - tokio::spawn(server); - format!("http://127.0.0.1:{port}") + let address = format!("http://127.0.0.1:{port}"); + + let mut configuration = get_configuration().expect("Failed to read configurations"); + configuration.database.database_name = Uuid::new_v4().to_string(); + let connectionn_pool = configure_database(&configuration.database).await; + + let server = zero2prod::startup::run(listener, connectionn_pool.clone()) + .expect("Failed to bind address"); + let _ = tokio::spawn(server); + + TestApp { + address, + db_pool: connectionn_pool, + } +} + +/// Create a new database for testing +/// +/// This function creates a new database for testing and returns a connection pool to it. +/// This is important in order to avoid polluting an existing database with test data. +pub async fn configure_database(config: &zero2prod::configurations::DatabaseSettings) -> PgPool { + // Create database + let mut connection = PgConnection::connect(&config.get_connnection_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.get_connnection_string()) + .await + .expect("Failed to connect to Postgres"); + sqlx::migrate!("./migrations") + .run(&connection_pool) + .await + .expect("Failed to migrate database"); + + connection_pool } #[tokio::test] async fn health_check_works() { // Arrange - let address = spawn_app(); + let address = spawn_app().await.address; // Act let client = reqwest::Client::new(); let response = client @@ -30,19 +73,14 @@ async fn health_check_works() { #[tokio::test] async fn subscribe_returns_a_200_for_valid_form_data() { // Arrange - let app_address = spawn_app(); - let configuration = get_configuration().expect("Failed to read configurations"); - let connection_string = configuration.database.get_connnection_string(); - let mut connection = sqlx::PgConnection::connect(&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"; // Act let response = client - .post(format!("{}/subscriptions", app_address)) + .post(format!("{}/subscriptions", app.address)) .header("Content-Type", "application/x-www-form-urlencoded") .body(body) .send() @@ -53,7 +91,7 @@ async fn subscribe_returns_a_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."); @@ -64,7 +102,7 @@ async fn subscribe_returns_a_200_for_valid_form_data() { #[tokio::test] async fn subscribe_returns_a_400_when_data_is_missing() { // Arrange - let app_address = spawn_app(); + let app_address = spawn_app().await.address; let client = reqwest::Client::new(); let test_cases = vec![ ("name=le%20guin", "missing the email"),