From dd8eb7ae7593740ba0a9302845a72d38cd37b5df Mon Sep 17 00:00:00 2001 From: runningwater Date: Tue, 30 Jul 2024 18:25:51 +0800 Subject: [PATCH] feature: add /subscriptions endpoint and refactor code --- .env | 1 + .github/workflows/rust.yml | 96 +++++++++++-- Cargo.lock | 276 +++++++++++++++++++++++++++++++++++- Cargo.toml | 2 + configuration.yaml | 7 + deny.toml | 1 + src/configuration.rs | 39 +++++ src/lib.rs | 39 +---- src/main.rs | 14 +- src/routes/health_check.rs | 5 + src/routes/mod.rs | 5 + src/routes/subscriptions.rs | 33 +++++ src/startup.rs | 24 ++++ tests/health_check.rs | 98 +++++++++---- 14 files changed, 565 insertions(+), 75 deletions(-) create mode 100644 .env create mode 100644 configuration.yaml create mode 100644 src/configuration.rs create mode 100644 src/routes/health_check.rs create mode 100644 src/routes/mod.rs create mode 100644 src/routes/subscriptions.rs create mode 100644 src/startup.rs diff --git a/.env b/.env new file mode 100644 index 0000000..88cfb53 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL="postgres://postgres:password@localhost:5432/newsletter" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ca7a996..ffd219e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -1,18 +1,56 @@ name: Rust -on: [push, pull_request] +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened] + branches: + - main env: CARGO_TERM_COLOR: always + SQLX_VERSION: 0.8.0 + SQLX_FEATURES: "chrono,macros,migrate,postgres,runtime-tokio,tls-rustls,uuid" jobs: test: name: Test runs-on: ubuntu-latest + services: + # Label used to access the service container + postgres: + # Docker Hub image + image: postgres:14 + # Environment variables scoped only for the `postgres` element + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: postgres + # When you map ports using the ports keyword, GitHub uses the --publish command to publish the container’s ports to the Docker host + # Opens tcp port 5432 on the host and service container + ports: + - 5432:5432 steps: - - uses: actions/checkout@v3 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - name: Check out repository code + uses: actions/checkout@v3 + - name: Install the Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Rust cache cleanup + uses: Swatinem/rust-cache@v2 + with: + key: sqlx-${{env.SQLX_VERSION}} + - name: Install slqx-client + run: cargo install sqlx-cli + --version={{env.SQLX_VERSION}} + --features ${{env.SQLX_FEATURES}} + --no-default-features + --locked + - name: Install postgresql-client + run: sudo apt-get update && sudo apt-get install postgresql-client -y + - name: Migrate database + run: SKIP_DOCKER=true ./scripts/init_db.sh - name: Run tests run: cargo test @@ -30,24 +68,64 @@ jobs: clippy: name: Clippy runs-on: ubuntu-latest + services: + postgres: + image: postgres:14 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: postgres + ports: + - 5432:5432 steps: - uses: actions/checkout@v3 - uses: dtolnay/rust-toolchain@stable with: components: clippy - uses: Swatinem/rust-cache@v2 + with: + key: sqlx-${{env.SQLX_VERSION}} + - name: Install sqlx-client + run: cargo install sqlx-cli + --version={{env.SQLX_VERSION}} + --features ${{env.SQLX_FEATURES}} + --no-default-features + --locked + - name: Install postgresql-client + run: sudo apt-get update && sudo apt-get install postgresql-client -y + - name: Migrate database + run: SKIP_DOCKER=true ./scripts/init_db.sh - name: Linting run: cargo clippy -- -D warnings coverage: name: Code coverage runs-on: ubuntu-latest - container: - image: xd009642/tarpaulin - options: --security-opt seccomp=unconfined + services: + postgres: + image: postgres:14 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password + POSTGRES_DB: postgres + ports: + - 5432:5432 steps: - name: Checkout repository uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + - name: Install postgresql-client + run: sudo apt-get update && sudo apt-get install postgresql-client -y + - uses: Swatinem/rust-cache@v2 + with: + key: sqlx-${{ env.SQLX_VERSION }} + - name: Install sqlx-cli + run: cargo install sqlx-cli + --version=${{ env.SQLX_VERSION }} + --features ${{ env.SQLX_FEATURES }} + --no-default-features + --locked + - name: Migrate database + run: SKIP_DOCKER=true ./scripts/init_db.sh - name: Generate code coverage - run: | - cargo tarpaulin --verbose --workspace + run: cargo install cargo-tarpaulin && cargo tarpaulin --verbose --workspace diff --git a/Cargo.lock b/Cargo.lock index bab5c4e..8743ba0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,6 +257,17 @@ dependencies = [ "libc", ] +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atoi" version = "2.0.0" @@ -420,18 +431,67 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "async-trait", + "convert_case 0.6.0", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.16.2" @@ -507,6 +567,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -543,7 +609,7 @@ version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", @@ -562,6 +628,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -818,6 +893,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashbrown" version = "0.14.5" @@ -834,7 +915,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1052,7 +1133,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -1085,6 +1166,17 @@ 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" @@ -1123,6 +1215,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -1362,6 +1460,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "parking" version = "2.2.0" @@ -1397,6 +1505,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1412,6 +1526,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -1642,6 +1801,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.6.0", + "serde", + "serde_derive", +] + [[package]] name = "rsa" version = "0.9.6" @@ -1662,6 +1833,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1851,6 +2032,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -1993,7 +2183,7 @@ dependencies = [ "futures-intrusive", "futures-io", "futures-util", - "hashbrown", + "hashbrown 0.14.5", "hashlink", "hex", "indexmap", @@ -2284,6 +2474,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -2361,6 +2560,40 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fb9f64314842840f1d940ac544da178732128f1c78c21772e876579e0da1db" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9f8729f5aea9562aac1cc0441f5d6de3cff1ee0c5d67293eeca5eb36ee7c16" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -2432,6 +2665,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[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.15" @@ -2459,6 +2698,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -2487,6 +2732,9 @@ name = "uuid" version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", +] [[package]] name = "vcpkg" @@ -2761,6 +3009,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.6.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.52.0" @@ -2771,14 +3028,25 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zero2prod" version = "0.1.0" dependencies = [ "actix-web", + "config", "reqwest", "serde", "sqlx", + "uuid", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 91dce30..1d0a9fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,9 @@ path = "src/lib.rs" [dependencies] actix-web = "4.8.0" +config = { version = "0.14.0", features = ["yaml"] } serde = { version = "1.0.204", features = ["derive"] } +uuid = { version = "1.10.0", features = ["v4"] } [dependencies.sqlx] version = "0.8.0" diff --git a/configuration.yaml b/configuration.yaml new file mode 100644 index 0000000..aef9981 --- /dev/null +++ b/configuration.yaml @@ -0,0 +1,7 @@ +application_port: 8080 +database: + host: "127.0.0.1" + port: 5432 + username: "postgres" + password: "password" + database_name: "newsletter" diff --git a/deny.toml b/deny.toml index 6b81a0f..8390655 100644 --- a/deny.toml +++ b/deny.toml @@ -95,6 +95,7 @@ allow = [ "BSD-3-Clause", "MPL-2.0", "ISC", + "CC0-1.0", ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the diff --git a/src/configuration.rs b/src/configuration.rs new file mode 100644 index 0000000..8f34873 --- /dev/null +++ b/src/configuration.rs @@ -0,0 +1,39 @@ +use config::{builder::DefaultState, ConfigBuilder}; + +#[derive(serde::Deserialize)] +pub struct Settings { + pub database: DatabaseSettings, + pub application_port: u16, +} + +#[derive(serde::Deserialize)] +pub struct DatabaseSettings { + pub host: String, + pub port: u16, + pub username: String, + pub password: String, + pub database_name: String, +} + +pub fn get_configuration() -> Result { + let config = ConfigBuilder::::default() + .add_source(config::File::with_name("configuration")) + .build()?; + + config.try_deserialize::() +} + +impl DatabaseSettings { + pub fn connection_string(&self) -> String { + format!( + "postgres://{}:{}@{}:{}/{}", + 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 + ) + } +} diff --git a/src/lib.rs b/src/lib.rs index 85b9890..300e4b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,35 +1,6 @@ -use serde::Deserialize; -use std::net::TcpListener; +pub mod configuration; +pub mod routes; +pub mod startup; -use actix_web::{ - dev::Server, - web::{self, get, post}, - App, HttpResponse, HttpServer, -}; - -async fn health_check() -> HttpResponse { - HttpResponse::Ok().finish() -} - -#[derive(Deserialize)] -#[allow(dead_code)] -struct FormData { - name: String, - email: String, -} - -async fn subscribe(_form: web::Form) -> HttpResponse { - HttpResponse::Ok().finish() -} - -pub fn run(listener: TcpListener) -> Result { - let server = HttpServer::new(|| { - App::new() - .route("/health_check", get().to(health_check)) - .route("/subscriptions", post().to(subscribe)) - }) - .listen(listener)? - .run(); - - Ok(server) -} +pub use configuration::*; +pub use startup::run; diff --git a/src/main.rs b/src/main.rs index d6a5d9c..5e4f673 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,15 @@ +use sqlx::PgPool; use std::net::TcpListener; - -use zero2prod::run; +use zero2prod::{get_configuration, startup::run}; #[actix_web::main] // or #[tokio::main] async fn main() -> std::io::Result<()> { - let listener = TcpListener::bind("127.0.0.1:8080").unwrap(); - run(listener)?.await + let config = get_configuration().expect("Failed to load configuration"); + let connection = PgPool::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).unwrap(); + run(listener, connection)?.await } diff --git a/src/routes/health_check.rs b/src/routes/health_check.rs new file mode 100644 index 0000000..d7eb4e0 --- /dev/null +++ b/src/routes/health_check.rs @@ -0,0 +1,5 @@ +use actix_web::HttpResponse; + +pub async fn health_check() -> HttpResponse { + HttpResponse::Ok().finish() +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..90ffeed --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,5 @@ +mod health_check; +mod subscriptions; + +pub use health_check::*; +pub use subscriptions::*; diff --git a/src/routes/subscriptions.rs b/src/routes/subscriptions.rs new file mode 100644 index 0000000..ee1faba --- /dev/null +++ b/src/routes/subscriptions.rs @@ -0,0 +1,33 @@ +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use sqlx::{types::chrono::Utc, PgPool}; +use uuid::Uuid; + +#[derive(Deserialize)] +#[allow(dead_code)] +pub struct FormData { + name: String, + email: String, +} + +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.as_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 new file mode 100644 index 0000000..31d7bcf --- /dev/null +++ b/src/startup.rs @@ -0,0 +1,24 @@ +use std::net::TcpListener; + +use actix_web::{ + dev::Server, + web::{self, get, post}, + App, HttpServer, +}; +use sqlx::PgPool; + +use crate::routes::{health_check, subscribe}; + +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", get().to(health_check)) + .route("/subscriptions", 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 56e627d..303d023 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -1,33 +1,83 @@ +use sqlx::{Connection, Executor, PgConnection, PgPool}; use std::net::TcpListener; -// You can inspect what code gets generated using -// `cargo expand --test health_check` (<- name of the test file) +use uuid::Uuid; +use zero2prod::configuration::{get_configuration, DatabaseSettings}; +use zero2prod::startup::run; + +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"); + // We retrieve the port assigned to us by the OS + let port = listener.local_addr().unwrap().port(); + 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 _f = actix_web::rt::spawn(server); + 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)) + .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 +} + #[actix_web::test] async fn health_check_works() { // Arrange - let address = spawn_app(); - // We need to bring in `reqwest` - // to perform HTTP requests against our application. + let app = spawn_app().await; let client = reqwest::Client::new(); + // Act let response = client - .get(format!("{}/health_check", address)) + // Use the returned application address + .get(&format!("{}/health_check", &app.address)) .send() .await .expect("Failed to execute request."); + // Assert assert!(response.status().is_success()); assert_eq!(Some(0), response.content_length()); } #[actix_web::test] -async fn subscribe_return_200_for_valid_form_data() { - let address = spawn_app(); +async fn subscribe_returns_a_200_for_valid_form_data() { + // Arrange + let app = spawn_app().await; let client = reqwest::Client::new(); + let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; // Act - let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; let response = client - .post(&format!("{}/subscriptions", &address)) + .post(&format!("{}/subscriptions", &app.address)) .header("Content-Type", "application/x-www-form-urlencoded") .body(body) .send() @@ -36,11 +86,20 @@ async fn subscribe_return_200_for_valid_form_data() { // Assert assert_eq!(200, response.status().as_u16()); + + let saved = sqlx::query!("SELECT email, name FROM subscriptions",) + .fetch_one(&app.db_pool) + .await + .expect("Failed to fetch saved subscription."); + + assert_eq!(saved.email, "ursula_le_guin@gmail.com"); + assert_eq!(saved.name, "le guin"); } #[actix_web::test] -async fn subscribe_return_a_400_when_data_is_missing() { - let address = spawn_app(); +async fn subscribe_returns_a_400_when_data_is_missing() { + // Arrange + let app = spawn_app().await; let client = reqwest::Client::new(); let test_cases = vec![ ("name=le%20guin", "missing the email"), @@ -51,12 +110,14 @@ async fn subscribe_return_a_400_when_data_is_missing() { for (invalid_body, error_message) in test_cases { // Act let response = client - .post(&format!("{}/subscriptions", &address)) + .post(&format!("{}/subscriptions", &app.address)) .header("Content-Type", "application/x-www-form-urlencoded") .body(invalid_body) .send() .await .expect("Failed to execute request."); + + // Assert assert_eq!( 400, response.status().as_u16(), @@ -66,14 +127,3 @@ async fn subscribe_return_a_400_when_data_is_missing() { ); } } - -// Launch our application in the background ~somehow~ -fn spawn_app() -> String { - let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port"); - // We retrieve the port assigned to us by the OS - let port = listener.local_addr().unwrap().port(); - let server = zero2prod::run(listener).expect("Failed to bind address"); - let _ignore = actix_web::rt::spawn(server); - - format!("http://127.0.0.1:{}", port) -}