diff --git a/Cargo.lock b/Cargo.lock index a9031b5..b8afafd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,16 @@ dependencies = [ "libc", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-trait" version = "0.1.68" @@ -374,6 +384,24 @@ dependencies = [ "typenum", ] +[[package]] +name = "deadpool" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63dfa964fe2a66f3fde91fc70b267fe193d822c7e603e2a675a49a7f46ad3f49" + [[package]] name = "der" version = "0.7.8" @@ -550,6 +578,21 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.28" @@ -594,6 +637,17 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +[[package]] +name = "futures-macro" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "futures-sink" version = "0.3.28" @@ -612,8 +666,10 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1076,6 +1132,7 @@ dependencies = [ "unicode-segmentation", "uuid", "validator", + "wiremock", ] [[package]] @@ -3007,6 +3064,30 @@ dependencies = [ "winapi", ] +[[package]] +name = "wiremock" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec874e1eef0df2dcac546057fe5e29186f09c378181cd7b635b4b7bcc98e9d81" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64", + "deadpool", + "futures", + "http 1.0.0", + "http-body-util", + "hyper 1.1.0", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index 6b24438..190c3c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,9 +59,11 @@ default-features = false features = ["json", "rustls-tls"] [dev-dependencies] +tokio = { version = "1", features = ["rt", "macros"] } once_cell = "1" claims = "0.7" fake = "2.9.2" quickcheck = "1.0.3" quickcheck_macros = "1.0.0" rand = "0.8.5" +wiremock = "0.6.0" diff --git a/configuration/base.yaml b/configuration/base.yaml index a5fed5a..bdd290c 100644 --- a/configuration/base.yaml +++ b/configuration/base.yaml @@ -9,3 +9,4 @@ database: email_client: base_url: "localhost" sender_email: "test@fmail.com" + authorization_token: "my-secret-token" diff --git a/src/configuration.rs b/src/configuration.rs index e5512ba..98f7a23 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -21,6 +21,7 @@ pub struct Settings { pub struct EmailClientSettings { pub base_url: String, pub sender_email: String, + pub authorization_token: Secret, } impl EmailClientSettings { diff --git a/src/email_client.rs b/src/email_client.rs index 551eb22..bd1203b 100644 --- a/src/email_client.rs +++ b/src/email_client.rs @@ -1,19 +1,26 @@ use crate::domain::SubscriberEmail; use reqwest::Client; +use secrecy::{ExposeSecret, Secret}; #[derive(Clone)] pub struct EmailClient { http_client: Client, base_url: String, sender: SubscriberEmail, + authorization_token: Secret, } impl EmailClient { - pub fn new(base_url: String, sender: SubscriberEmail) -> Self { + pub fn new( + base_url: String, + sender: SubscriberEmail, + authorization_token: Secret, + ) -> Self { Self { http_client: Client::new(), base_url, sender, + authorization_token, } } @@ -24,6 +31,69 @@ impl EmailClient { html_content: &str, text_content: &str, ) -> Result<(), String> { - todo!() + // TODO: use `reqwest::Url::join` and change `base_url`'s type from `String` to `reqwest::Url` + let url = format!("{}/email", self.base_url); + let request_body = SendEmailRequest { + from: self.sender.as_ref().to_owned(), + to: recipient.as_ref().to_owned(), + subject: subject.to_owned(), + html_body: html_content.to_owned(), + text_body: text_content.to_owned(), + }; + let builder = self + .http_client + .post(&url) + .header( + "X-Postmark-Server-Token", + self.authorization_token.expose_secret(), + ) + .json(&request_body); + Ok(()) + } +} +#[derive(serde::Serialize)] +struct SendEmailRequest { + from: String, + to: String, + subject: String, + html_body: String, + text_body: String, +} + +#[cfg(test)] +mod tests { + + use crate::domain::SubscriberEmail; + use crate::email_client::EmailClient; + use fake::faker::internet::en::SafeEmail; + use fake::faker::lorem::en::{Paragraph, Sentence}; + use fake::{Fake, Faker}; + use secrecy::Secret; + use wiremock::matchers::any; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + #[tokio::test] + async fn send_email_fires_a_request_to_base_url() { + // Arrange + let mock_server = MockServer::start().await; + let sender = SubscriberEmail::parse(SafeEmail().fake()).unwrap(); + let email_client = EmailClient::new(mock_server.uri(), sender, Secret::new(Faker.fake())); + + Mock::given(any()) + .respond_with(ResponseTemplate::new(200)) + .expect(1) + .mount(&mock_server) + .await; + + let subscriber_email = SubscriberEmail::parse(SafeEmail().fake()).unwrap(); + let subject: String = Sentence(1..2).fake(); + let content: String = Paragraph(1..10).fake(); + + // Act + let _ = email_client + .send_email(subscriber_email, &subject, &content, &content) + .await; + + // Assert } } diff --git a/src/main.rs b/src/main.rs index 1ff7c2e..b479757 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,11 @@ async fn main() { .email_client .sender() .expect("Invalid sender email address."); - let email_client = EmailClient::new(configuration.email_client.base_url, sender_email); + let email_client = EmailClient::new( + configuration.email_client.base_url, + sender_email, + configuration.email_client.authorization_token, + ); startup::run(listener, connection_pool, email_client) .await .unwrap(); diff --git a/tests/health_check.rs b/tests/health_check.rs index eea402e..a02064f 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -153,7 +153,11 @@ async fn spawn_app() -> TestApp { .email_client .sender() .expect("Invalid sender email address."); - let email_client = EmailClient::new(configuration.email_client.base_url, sender_email); + let email_client = EmailClient::new( + configuration.email_client.base_url, + sender_email, + configuration.email_client.authorization_token, + ); let service = learn_axum::startup::app(connection_pool.clone(), email_client); tokio::spawn(async move {