diff --git a/Cargo.lock b/Cargo.lock index b550cc2..71aee95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1049,6 +1049,15 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linkify" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780" +dependencies = [ + "memchr", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -2996,6 +3005,7 @@ dependencies = [ "claims", "config", "fake", + "linkify", "once_cell", "proptest", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index 637e6c2..35e6aeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ validator = "0.16.1" [dev-dependencies] claims = "0.7.1" fake = "2.9.2" +linkify = "0.10.0" proptest = "1.4.0" serde_json = "1.0.114" tokio = { version = "1.36.0", features = ["macros", "rt"] } diff --git a/src/routes/subscriptions.rs b/src/routes/subscriptions.rs index 434cdfc..f1a939d 100644 --- a/src/routes/subscriptions.rs +++ b/src/routes/subscriptions.rs @@ -1,8 +1,10 @@ use crate::{ app_state::AppState, domain::{NewSubscriber, SubscriberEmail, SubscriberName}, + email_client::EmailClient, }; use axum::{extract::State, http::StatusCode, routing::post, Form, Router}; +use reqwest::Error; use serde::Deserialize; use sqlx::PgPool; use time::OffsetDateTime; @@ -36,14 +38,7 @@ async fn subscribe(State(app_state): State, Form(form): Form return StatusCode::INTERNAL_SERVER_ERROR; } - if app_state - .email_client - .send_email( - new_subscriber.email, - "Welcome!", - "Welcome to our newsletter!", - "Welcome to our newsletter!", - ) + if send_confirmation_email(&app_state.email_client, new_subscriber) .await .is_err() { @@ -81,6 +76,28 @@ async fn insert_subscriber( Ok(()) } +#[tracing::instrument( + name = "Sending confirmation email to a new subscriber", + skip(email_client, new_subscriber) +)] +async fn send_confirmation_email( + email_client: &EmailClient, + new_subscriber: NewSubscriber, +) -> Result<(), Error> { + let confirmation_link = "https://there-is-no-such-domain.com/subscriptions/confirm"; + let html_body = format!( + "Welcome to our newsletter!
\ + Click here to confirm your subscription." + ); + let plain_body = format!( + "Welcome to our newsletter!\nVisit {confirmation_link} to confirm your subscription." + ); + + email_client + .send_email(new_subscriber.email, "Welcome!", &html_body, &plain_body) + .await +} + #[derive(Deserialize)] struct FormData { name: String, diff --git a/tests/api/subscriptions.rs b/tests/api/subscriptions.rs index 9c97410..1455a90 100644 --- a/tests/api/subscriptions.rs +++ b/tests/api/subscriptions.rs @@ -1,10 +1,11 @@ +use crate::helpers::TestApp; +use linkify::{LinkFinder, LinkKind}; +use serde_json::Value; use wiremock::{ matchers::{method, path}, Mock, ResponseTemplate, }; -use crate::helpers::TestApp; - #[tokio::test] async fn subscribe_returns_a_200_for_valid_form_data() { // given @@ -105,3 +106,36 @@ async fn subscribe_sends_a_confirmation_email_for_valid_data() { // then assert } + +#[tokio::test] +async fn subscribe_sends_a_confirmation_email_with_a_link() { + // given + let app = TestApp::spawn().await; + let body = "name=Imi%C4%99%20Nazwisko&email=imie.nazwisko%40example.com"; + + Mock::given(path("/email")) + .and(method("POST")) + .respond_with(ResponseTemplate::new(200)) + .expect(1) + .mount(&app.email_server) + .await; + + // when + app.post_subscriptions(body.into()).await; + + // then + let request = &app.email_server.received_requests().await.unwrap()[0]; + let body: Value = serde_json::from_slice(&request.body).unwrap(); + let get_link = |s: &str| { + let links: Vec<_> = LinkFinder::new() + .links(s) + .filter(|l| *l.kind() == LinkKind::Url) + .collect(); + assert_eq!(links.len(), 1); + links[0].as_str().to_owned() + }; + let html_link = get_link(&body["HtmlBody"].as_str().unwrap()); + let text_link = get_link(&body["TextBody"].as_str().unwrap()); + + assert_eq!(html_link, text_link); +}