diff --git a/email_newsletter/Cargo.toml b/email_newsletter/Cargo.toml new file mode 100644 index 0000000..4f49e36 --- /dev/null +++ b/email_newsletter/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "email_newsletter" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +path = "src/lib.rs" + +[[bin]] +path = "src/main.rs" +name = "email_newsletter" + +[dependencies] +actix-web = "4" +serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } + +[dev-dependencies] +reqwest = "0.11" diff --git a/email_newsletter/src/lib.rs b/email_newsletter/src/lib.rs new file mode 100644 index 0000000..dd70b53 --- /dev/null +++ b/email_newsletter/src/lib.rs @@ -0,0 +1,28 @@ +use actix_web::{dev::Server, web, App, HttpResponse, HttpServer}; +use std::net::TcpListener; + +async fn health_check() -> HttpResponse { + HttpResponse::Ok().finish() +} + +#[derive(serde::Deserialize)] +struct FormData { + email: String, + name: 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", web::get().to(health_check)) + .route("/subscriptions", web::post().to(subscribe)) + }) + .listen(listener)? + .run(); + + Ok(server) +} diff --git a/email_newsletter/src/main.rs b/email_newsletter/src/main.rs new file mode 100644 index 0000000..0e60fc4 --- /dev/null +++ b/email_newsletter/src/main.rs @@ -0,0 +1,10 @@ +use std::net::TcpListener; + +use email_newsletter::run; + +#[tokio::main] +async fn main() -> Result<(), std::io::Error> { + let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to address"); + + run(listener)?.await +} diff --git a/email_newsletter/tests/health_check.rs b/email_newsletter/tests/health_check.rs new file mode 100644 index 0000000..7d2b2e5 --- /dev/null +++ b/email_newsletter/tests/health_check.rs @@ -0,0 +1,71 @@ +use std::net::TcpListener; + +fn spawn_app() -> String { + let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to random port"); + let port = listener.local_addr().unwrap().port(); + let server = email_newsletter::run(listener).expect("Failed to bind address"); + + let _ = tokio::spawn(server); + + format!("http://127.0.0.1:{}", port) +} + +#[tokio::test] +async fn health_check_works() { + let address = spawn_app(); + let client = reqwest::Client::new(); + + let response = client + .get(&format!("{}/health_check", &address)) + .send() + .await + .expect("Failed to execute request"); + + assert!(response.status().is_success()); + assert_eq!(Some(0), response.content_length()); +} + +#[tokio::test] +async fn subscribe_returns_a_200_for_valid_form_data() { + let app_address = spawn_app(); + let client = reqwest::Client::new(); + + let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; + let response = client + .post(&format!("{}/subscriptions", &app_address)) + .header("Content-type", "application/x-www-form-urlencoded") + .body(body) + .send() + .await + .expect("Failed to execute request"); + + assert_eq!(200, response.status().as_u16()); +} + +#[tokio::test] +async fn subscribe_returns_a_400_when_data_is_missing() { + let app_address = spawn_app(); + let client = reqwest::Client::new(); + let test_cases = vec![ + ("name=le%20guin", "missing the email"), + ("email=ursula_le_guin%40gmail.com", "missing the name"), + ("", "missing both name and email"), + ]; + + for (invalid_body, error_message) in test_cases { + let response = client + .post(&format!("{}/subscriptions", &app_address)) + .header("Content-type", "application/x-www-form-urlencoded") + .body(invalid_body) + .send() + .await + .expect("Failed to execute request"); + + assert_eq!( + 400, + response.status().as_u16(), + "The API did not fail with 400 Bad Request when the payload was {}", + error_message + ); + } +}