test(wiremoc): add tests

This commit is contained in:
Kristofers Solo 2024-04-06 14:20:38 +03:00
parent a68c95d51b
commit 241fe4953f
12 changed files with 334 additions and 188 deletions

324
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.79"
@ -265,16 +275,6 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
@ -330,6 +330,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"
@ -377,15 +395,6 @@ dependencies = [
"serde",
]
[[package]]
name = "encoding_rs"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
dependencies = [
"cfg-if",
]
[[package]]
name = "env_logger"
version = "0.7.1"
@ -467,21 +476,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@ -491,6 +485,21 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.30"
@ -535,6 +544,17 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
]
[[package]]
name = "futures-sink"
version = "0.3.30"
@ -553,8 +573,10 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
@ -765,18 +787,19 @@ dependencies = [
]
[[package]]
name = "hyper-tls"
version = "0.6.0"
name = "hyper-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
dependencies = [
"bytes",
"http-body-util",
"futures-util",
"http",
"hyper",
"hyper-util",
"native-tls",
"rustls 0.22.3",
"rustls-pki-types",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tower-service",
]
@ -1000,24 +1023,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nom"
version = "7.1.3"
@ -1116,50 +1121,6 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "openssl"
version = "0.10.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
dependencies = [
"bitflags 2.5.0",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "overload"
version = "0.1.1"
@ -1460,37 +1421,36 @@ checksum = "e333b1eb9fe677f6893a9efcb0d277a2d3edd83f358a236b657c32301dc6e5f6"
dependencies = [
"base64",
"bytes",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"http-body-util",
"hyper",
"hyper-tls",
"hyper-rustls",
"hyper-util",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"rustls 0.22.3",
"rustls-pemfile",
"rustls-pki-types",
"serde",
"serde_json",
"serde_urlencoded",
"sync_wrapper",
"system-configuration",
"tokio",
"tokio-native-tls",
"tokio-rustls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"webpki-roots 0.26.1",
"winreg",
]
@ -1555,10 +1515,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba"
dependencies = [
"ring",
"rustls-webpki",
"rustls-webpki 0.101.7",
"sct",
]
[[package]]
name = "rustls"
version = "0.22.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c"
dependencies = [
"log",
"ring",
"rustls-pki-types",
"rustls-webpki 0.102.2",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
@ -1568,6 +1542,12 @@ dependencies = [
"base64",
]
[[package]]
name = "rustls-pki-types"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247"
[[package]]
name = "rustls-webpki"
version = "0.101.7"
@ -1578,6 +1558,17 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustls-webpki"
version = "0.102.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.14"
@ -1590,15 +1581,6 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]]
name = "schannel"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
@ -1625,29 +1607,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "serde"
version = "1.0.197"
@ -1752,15 +1711,6 @@ dependencies = [
"lazy_static",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "2.2.0"
@ -1873,7 +1823,7 @@ dependencies = [
"once_cell",
"paste",
"percent-encoding",
"rustls",
"rustls 0.21.10",
"rustls-pemfile",
"serde",
"serde_json",
@ -1886,7 +1836,7 @@ dependencies = [
"tracing",
"url",
"uuid",
"webpki-roots",
"webpki-roots 0.25.4",
]
[[package]]
@ -2082,27 +2032,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"system-configuration-sys",
]
[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tempfile"
version = "3.10.1"
@ -2202,11 +2131,10 @@ dependencies = [
"libc",
"mio",
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"tracing",
"windows-sys 0.48.0",
]
@ -2222,12 +2150,13 @@ dependencies = [
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
name = "tokio-rustls"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f"
dependencies = [
"native-tls",
"rustls 0.22.3",
"rustls-pki-types",
"tokio",
]
@ -2646,6 +2575,15 @@ version = "0.25.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
[[package]]
name = "webpki-roots"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "whoami"
version = "1.5.1"
@ -2838,6 +2776,30 @@ dependencies = [
"windows-sys 0.48.0",
]
[[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",
"http-body-util",
"hyper",
"hyper-util",
"log",
"once_cell",
"regex",
"serde",
"serde_json",
"tokio",
"url",
]
[[package]]
name = "zero2prod"
version = "0.1.0"
@ -2854,6 +2816,7 @@ dependencies = [
"secrecy",
"serde",
"serde-aux",
"serde_json",
"sqlx",
"tokio",
"tower-http",
@ -2864,6 +2827,7 @@ dependencies = [
"unicode-segmentation",
"uuid",
"validator",
"wiremock",
]
[[package]]

View File

@ -27,7 +27,12 @@ sqlx = { version = "0.7", default-features = false, features = [
"chrono",
"migrate",
] }
tokio = { version = "1.36", features = ["full"] }
tokio = { version = "1.36", features = [
"rt",
"macros",
"tracing",
"rt-multi-thread",
] }
uuid = { version = "1.8", features = ["v4", "serde"] }
tracing = { version = "0.1", features = ["log"] }
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
@ -39,13 +44,18 @@ serde-aux = "4"
unicode-segmentation = "1"
claims = "0.7"
validator = "0.16"
reqwest = { version = "0.12", default-features = false, features = [
"json",
"rustls-tls",
] }
[dev-dependencies]
reqwest = "0.12"
once_cell = "1.19"
fake = "~2.3"
quickcheck = "0.9"
quickcheck_macros = "0.9"
wiremock = "0.6"
serde_json = "1"
[package.metadata.clippy]
warn = [

View File

@ -7,3 +7,8 @@ port = 5432
username = "postgres"
password = "password"
database_name = "newsletter"
[email_client]
base_url = "localhost"
sender_email = "test@gmail.com"
auth_token = "super-secret-token"

View File

@ -3,3 +3,7 @@ host = "0.0.0.0"
[database]
require_ssl = true
[email_client]
base_url = "localhost"
sender_email = "zero2prod@kristofers.xyz" # FIX: swap to postmark

View File

@ -8,10 +8,13 @@ use sqlx::{
ConnectOptions,
};
use crate::domain::SubscriberEmail;
#[derive(Debug, Deserialize)]
pub struct Settings {
pub database: DatabaseSettings,
pub application: ApplicationSettings,
pub email_client: EmailClientSettings,
}
#[derive(Debug, Deserialize)]
@ -37,6 +40,18 @@ pub enum Environment {
Local,
Production,
}
#[derive(Debug, Deserialize)]
pub struct EmailClientSettings {
pub base_url: String,
pub sender_email: String,
pub auth_token: Secret<String>,
}
impl EmailClientSettings {
pub fn sender(&self) -> Result<SubscriberEmail, String> {
self.sender_email.clone().try_into()
}
}
pub fn get_config() -> Result<Settings, config::ConfigError> {
let base_path = std::env::current_dir().expect("Failed to determine current directory");

View File

@ -2,7 +2,7 @@ use std::str::FromStr;
use validator::validate_email;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct SubscriberEmail(String);
impl TryFrom<String> for SubscriberEmail {

121
src/email_client.rs Normal file
View File

@ -0,0 +1,121 @@
use reqwest::{Client, Url};
use secrecy::{ExposeSecret, Secret};
use serde::Serialize;
use crate::domain::SubscriberEmail;
#[derive(Debug, Clone)]
pub struct EmailClient {
http_client: Client,
base_url: String,
sender: SubscriberEmail,
auth_token: Secret<String>,
}
impl EmailClient {
pub fn new(base_url: String, sender: SubscriberEmail, auth_token: Secret<String>) -> Self {
Self {
http_client: Client::new(),
base_url,
sender,
auth_token,
}
}
pub async fn send_email(
&self,
recipient: SubscriberEmail,
subject: &str,
html_content: &str,
text_content: &str,
) -> Result<(), reqwest::Error> {
let url = Url::parse(&self.base_url)
.and_then(|path| path.join("email"))
.unwrap();
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.auth_token.expose_secret())
.json(&request_body)
.send()
.await?;
Ok(())
}
}
#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
struct SendEmailRequest {
from: String,
to: String,
subject: String,
html_body: String,
text_body: String,
}
#[cfg(test)]
mod tests {
use fake::{
faker::{
internet::en::SafeEmail,
lorem::en::{Paragraph, Sentence},
},
Fake, Faker,
};
use serde_json;
use wiremock::{
matchers::{header, header_exists, method, path},
Match, Mock, MockServer, ResponseTemplate,
};
struct SendEmailBodyMatcher;
impl Match for SendEmailBodyMatcher {
fn matches(&self, request: &wiremock::Request) -> bool {
match serde_json::from_slice::<serde_json::Value>(&request.body) {
Err(_) => false,
Ok(body) => {
dbg!(&body);
body.get("From").is_some()
&& body.get("To").is_some()
&& body.get("Subject").is_some()
&& body.get("HtmlBody").is_some()
&& body.get("TextBody").is_some()
}
}
}
}
use super::*;
#[tokio::test]
async fn send_email_sends_the_expectred_request() {
let mock_server = MockServer::start().await;
let sender = SafeEmail().fake::<String>().try_into().unwrap();
let email_client = EmailClient::new(mock_server.uri(), sender, Secret::new(Faker.fake()));
Mock::given(header_exists("X-Postmark-Server-Token"))
.and(header("Content-Type", "application/json"))
.and(path("/email"))
.and(method("POST"))
.and(SendEmailBodyMatcher)
.respond_with(ResponseTemplate::new(200))
.expect(1)
.mount(&mock_server)
.await;
let subscriber_email = SafeEmail().fake::<String>().try_into().unwrap();
let subject = Sentence(1..2).fake::<String>();
dbg!(&subject);
let content = Paragraph(1..10).fake::<String>();
let _ = email_client
.send_email(subscriber_email, &subject, &content, &content)
.await;
}
}

View File

@ -1,4 +1,5 @@
pub mod config;
pub mod domain;
pub mod email_client;
pub mod routes;
pub mod telemetry;

View File

@ -2,6 +2,7 @@ use sqlx::postgres::PgPoolOptions;
use tokio::net::TcpListener;
use zero2prod::{
config::get_config,
email_client::EmailClient,
routes::route,
telemetry::{get_subscriber, init_subscriber},
};
@ -17,5 +18,15 @@ async fn main() -> Result<(), std::io::Error> {
.await
.expect("Failed to bind port 8000.");
axum::serve(listener, route(pool)).await
let sender_email = config
.email_client
.sender()
.expect("Invalid sender email adress");
let email_client = EmailClient::new(
config.email_client.base_url,
sender_email,
config.email_client.auth_token,
);
axum::serve(listener, route(pool, email_client)).await
}

View File

@ -19,11 +19,14 @@ use tower_http::{classify::ServerErrorsFailureClass, trace::TraceLayer};
use tracing::{info_span, Span};
use uuid::Uuid;
pub fn route(state: PgPool) -> Router {
use crate::email_client::EmailClient;
pub fn route(pool: PgPool, email_client: EmailClient) -> Router {
Router::new()
.route("/health_check", get(health_check))
.route("/subscriptions", post(subscribe))
.with_state(state)
.with_state(pool)
.with_state(email_client)
.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &Request<_>| {

View File

@ -5,7 +5,7 @@ use sqlx::PgPool;
use tracing::error;
use uuid::Uuid;
use crate::domain::{NewSubscriber, SubscriberEmail, SubscriberName};
use crate::domain::NewSubscriber;
#[derive(Deserialize)]
pub struct FormData {

View File

@ -5,6 +5,7 @@ use tokio::net::TcpListener;
use uuid::Uuid;
use zero2prod::{
config::{get_config, DatabaseSettings},
email_client::EmailClient,
routes::route,
telemetry::{get_subscriber, init_subscriber},
};
@ -140,8 +141,19 @@ async fn spawn_app() -> TestApp {
let pool = configure_database(&config.database).await;
let pool_clone = pool.clone();
let sender_email = config
.email_client
.sender()
.expect("Invalid sender email adress");
let email_client = EmailClient::new(
config.email_client.base_url,
sender_email,
config.email_client.auth_token,
);
let _ = tokio::spawn(async move {
axum::serve(listener, route(pool_clone))
axum::serve(listener, route(pool_clone, email_client))
.await
.expect("Failed to bind address.")
});