diff --git a/src/routes/mod.rs b/src/routes/mod.rs index 90ffeed..d0ddba0 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -1,5 +1,7 @@ mod health_check; mod subscriptions; +mod subscriptions_confirm; pub use health_check::*; pub use subscriptions::*; +pub use subscriptions_confirm::*; diff --git a/src/routes/subscriptions.rs b/src/routes/subscriptions.rs index 25b3f99..05950be 100644 --- a/src/routes/subscriptions.rs +++ b/src/routes/subscriptions.rs @@ -100,15 +100,13 @@ pub async fn insert_subscriber( let _ = sqlx::query!( r#" INSERT INTO subscriptions (id, email, name, subscribed_at, status) - VALUES ($1, $2, $3, $4, 'confirmed') + VALUES ($1, $2, $3, $4, 'pending_confirmation') "#, Uuid::new_v4(), new_subscriber.email.as_ref(), new_subscriber.name.as_ref(), Utc::now() ) - // We use `get_ref` to get an immutable reference to the `PgConnection` - // wrapped by `web::Data`. .execute(db_pool) .await .map_err(|e| { diff --git a/src/routes/subscriptions_confirm.rs b/src/routes/subscriptions_confirm.rs new file mode 100644 index 0000000..7958e06 --- /dev/null +++ b/src/routes/subscriptions_confirm.rs @@ -0,0 +1,24 @@ +use crate::startup::AppState; +use axum::routing::post; +use axum::{ + extract::Query, + http::StatusCode, + response::{IntoResponse, Response}, + Router, +}; + +#[derive(serde::Deserialize)] +pub struct Parameters { + subscription_token: String, +} + +#[tracing::instrument(name = "Confirm a pending subscriber", skip(_parameters))] +pub async fn confirm(Query(_parameters): Query) -> Response { + return (StatusCode::OK,).into_response(); +} + +pub fn routes_subscriptions_confirm(state: AppState) -> Router { + Router::new() + .route("/subscriptions/confirm", post(confirm)) + .with_state(state) +} diff --git a/src/startup.rs b/src/startup.rs index c491757..cb020f8 100644 --- a/src/startup.rs +++ b/src/startup.rs @@ -65,7 +65,9 @@ impl Application { }; let app = Router::new() .merge(crate::routes::routes_health_check()) - .merge(crate::routes::routes_subscriptions(state)) + // TODO: check whether state cloning is what we want + .merge(crate::routes::routes_subscriptions(state.clone())) + .merge(crate::routes::routes_subscriptions_confirm(state)) .layer( // from https://docs.rs/tower-http/0.2.5/tower_http/request_id/index.html#using-trace ServiceBuilder::new() diff --git a/tests/api/main.rs b/tests/api/main.rs index 3b9c227..177847a 100644 --- a/tests/api/main.rs +++ b/tests/api/main.rs @@ -1,3 +1,4 @@ mod health_check; mod helpers; mod subscriptions; +mod subscriptions_confirm; diff --git a/tests/api/subscriptions.rs b/tests/api/subscriptions.rs index e9bae61..7643d7c 100644 --- a/tests/api/subscriptions.rs +++ b/tests/api/subscriptions.rs @@ -97,14 +97,31 @@ async fn subscribe_returns_a_200_for_valid_form_data() { // Assert assert_eq!(200, response.status().as_u16()); +} - let saved = sqlx::query!("SELECT email, name FROM subscriptions",) +#[tokio::test] +async fn subscribe_persists_the_new_subscriber() { + // Arrange + let app = spawn_app().await; + let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; + Mock::given(path("/email")) + .and(method("POST")) + .respond_with(ResponseTemplate::new(200)) + .mount(&app.email_server) + .await; + + // Act + app.post_subscriptions(body.into()).await; + + // Assert + let saved = sqlx::query!("SELECT email, name, status 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"); + assert_eq!(saved.status, "pending_confirmation"); } #[tokio::test] @@ -125,7 +142,7 @@ async fn subscribe_returns_a_400_when_fields_are_present_but_invalid() { assert_eq!( 400, response.status().as_u16(), - "The API did not return a 200 OK when the payload was {}.", + "The API did not fail with 400 when the payload was {}.", description ); } diff --git a/tests/api/subscriptions_confirm.rs b/tests/api/subscriptions_confirm.rs new file mode 100644 index 0000000..f8011fd --- /dev/null +++ b/tests/api/subscriptions_confirm.rs @@ -0,0 +1,15 @@ +use crate::helpers::spawn_app; + +#[tokio::test] +async fn confirmations_without_token_are_rejected_with_a_405() { + // Arrange + let app = spawn_app().await; + + // Act + let response = reqwest::get(&format!("{}/subscriptions/confirm", app.address)) + .await + .unwrap(); + + // Assert + assert_eq!(response.status().as_u16(), 405); +}