feat: add email client

This commit is contained in:
Sandro Eiler 2024-02-26 22:00:33 +01:00
parent 13db7853bd
commit ac216925ff
7 changed files with 167 additions and 4 deletions

81
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -9,3 +9,4 @@ database:
email_client:
base_url: "localhost"
sender_email: "test@fmail.com"
authorization_token: "my-secret-token"

View file

@ -21,6 +21,7 @@ pub struct Settings {
pub struct EmailClientSettings {
pub base_url: String,
pub sender_email: String,
pub authorization_token: Secret<String>,
}
impl EmailClientSettings {

View file

@ -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<String>,
}
impl EmailClient {
pub fn new(base_url: String, sender: SubscriberEmail) -> Self {
pub fn new(
base_url: String,
sender: SubscriberEmail,
authorization_token: Secret<String>,
) -> 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
}
}

View file

@ -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();

View file

@ -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 {