Skip to content

Commit

Permalink
Merge pull request #1 from dmcclung/email-service-work
Browse files Browse the repository at this point in the history
Email service work
  • Loading branch information
dmcclung authored Mar 9, 2024
2 parents 68051ea + 3c58786 commit f7bb66f
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 43 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ anyhow = "1.0.80"
dotenv = "0.15.0"
lettre = "0.11.4"
log = "0.4.20"
once_cell = "1.19.0"
regex = "1.10.3"
serde = { version = "1.0.195", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
Expand Down
34 changes: 26 additions & 8 deletions src/app.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,58 @@
use crate::config::Config;
use crate::{
config::Config,
email::{EmailSender, EmailService},
};
use anyhow::Result;
use sqlx::postgres::PgPoolOptions;
use std::net::TcpListener;
use std::{marker::PhantomData, net::TcpListener};

use actix_web::{dev::Server, middleware::Logger, web, App, HttpServer};
use sqlx::{Pool, Postgres};

use crate::routes::{health_check, subscribe};

pub struct Application {
pub struct Application<T> {
port: u16,
server: Server,
_marker: PhantomData<T>
}

impl Application {
pub async fn build(config: &Config, addr: String) -> Result<Self> {
impl<T> Application<T>
where
T: EmailSender + Send + Sync + 'static,
{
pub async fn build(
config: &Config,
addr: String,
email_service: EmailService<'static, T>,
) -> Result<Self> {
let pool = PgPoolOptions::new().connect(&config.db_config.url).await?;
sqlx::migrate!().run(&pool).await?;

let listener = TcpListener::bind(addr)?;
let port = listener.local_addr().unwrap().port();
let server = Self::run(listener, pool)?;
let server = Self::run(listener, pool, email_service)?;

Ok(Self { port, server })
Ok(Self { port, server, _marker: PhantomData })
}

fn run(listener: TcpListener, pool: Pool<Postgres>) -> Result<Server> {
fn run(
listener: TcpListener,
pool: Pool<Postgres>,
email_service: EmailService<'static, T>,
) -> Result<Server> {
let pool = web::Data::new(pool);
let email_service = web::Data::new(email_service);
let server = HttpServer::new(move || {
let pool = pool.clone();
let email_service = email_service.clone();

App::new()
.wrap(Logger::default())
.route("/health_check", web::get().to(health_check))
.route("/subscriptions", web::post().to(subscribe))
.app_data(pool)
.app_data(email_service)
})
.listen(listener)?
.run();
Expand Down
66 changes: 34 additions & 32 deletions src/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use anyhow::Result;

use tracing::info;

#[derive(Clone)]
pub struct SmtpConfig {
host: String,
port: u16,
Expand Down Expand Up @@ -98,13 +99,13 @@ pub struct Email<'a> {
}

pub trait EmailSender {
fn send(&mut self, port: u16, host: &str, creds: Credentials, message: Message) -> Result<()>;
fn send(&self, port: u16, host: &str, creds: Credentials, message: Message) -> Result<()>;
}

struct LettreEmailSender;
pub struct LettreEmailSender;

impl EmailSender for LettreEmailSender {
fn send(&mut self, port: u16, host: &str, creds: Credentials, message: Message) -> Result<()> {
fn send(&self, port: u16, host: &str, creds: Credentials, message: Message) -> Result<()> {
let mailer = SmtpTransport::relay(host)?
.port(port)
.credentials(creds)
Expand All @@ -119,11 +120,11 @@ impl EmailSender for LettreEmailSender {

pub struct EmailService<'a, T: EmailSender> {
config: SmtpConfig,
email_sender: &'a mut T,
email_sender: &'a T,
}

impl<'a, T: EmailSender> EmailService<'a, T> {
pub fn new(config: SmtpConfig, email_sender: &'a mut T) -> Self {
pub fn new(config: SmtpConfig, email_sender: &'a T) -> Self {
Self {
config,
email_sender,
Expand Down Expand Up @@ -174,9 +175,35 @@ mod tests {
use fake::Fake;
use lettre::Message;
use std::env::{remove_var, set_var};
use std::sync::{Arc, Mutex};

use super::{Email, EmailSender, EmailService, SmtpConfig};

pub struct MockEmailSender {
pub sent_messages: Arc<Mutex<Vec<Message>>>,
}

impl MockEmailSender {
fn new() -> Self {
Self {
sent_messages: Arc::new(Mutex::new(Vec::new())),
}
}
}

impl EmailSender for MockEmailSender {
fn send(
&self,
_port: u16,
_host: &str,
_creds: lettre::transport::smtp::authentication::Credentials,
message: lettre::Message,
) -> Result<()> {
self.sent_messages.lock().unwrap().push(message);
Ok(())
}
}

#[test]
fn smtp_config_from_env() {
let hostname = generate_hostname();
Expand Down Expand Up @@ -212,31 +239,6 @@ mod tests {
format!("smtp.{}.{}", domain, domain_suffix)
}

struct MockEmailSender {
sent_messages: Vec<Message>,
}

impl MockEmailSender {
fn new() -> Self {
Self {
sent_messages: Vec::new(),
}
}
}

impl EmailSender for MockEmailSender {
fn send(
&mut self,
_port: u16,
_host: &str,
_creds: lettre::transport::smtp::authentication::Credentials,
message: lettre::Message,
) -> Result<()> {
self.sent_messages.push(message);
Ok(())
}
}

#[test]
fn send_valid_email() {
let smtp_config = SmtpConfig::new(
Expand All @@ -246,7 +248,7 @@ mod tests {
Password(8..16).fake(),
SafeEmail().fake(),
);
let email_sender = &mut MockEmailSender::new();
let email_sender = &MockEmailSender::new();

let to: String = SafeEmail().fake();
let from: String = SafeEmail().fake();
Expand All @@ -267,6 +269,6 @@ mod tests {
let res = email_service.send_email(email);

assert_ok!(res);
assert_eq!(1, email_sender.sent_messages.len());
assert_eq!(1, email_sender.sent_messages.lock().unwrap().len());
}
}
7 changes: 6 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use anyhow::Result;
use zero2prod::config::Config;

use zero2prod::app::Application;
use zero2prod::email::{EmailService, LettreEmailSender};

#[tokio::main]
async fn main() -> Result<()> {
Expand All @@ -10,7 +11,11 @@ async fn main() -> Result<()> {
let config = Config::new();
let addr = format!("[::]:{}", config.port);

let app = Application::build(&config, addr).await?;
static EMAIL_SENDER: LettreEmailSender = LettreEmailSender {};

let email_service = EmailService::new(config.smtp_config.clone(), &EMAIL_SENDER);

let app = Application::build(&config, addr, email_service).await?;
app.run_until_stopped().await?;

Ok(())
Expand Down
36 changes: 34 additions & 2 deletions tests/api/utils.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,41 @@
//! tests/api/utils.rs

use anyhow::Result;
use std::sync::{Arc, Mutex};

use anyhow::Result;
use lettre::Message;
use once_cell::sync::Lazy;
use reqwest::Response;
use sqlx::postgres::PgPoolOptions;
use sqlx::{Pool, Postgres};
use zero2prod::app::Application;
use zero2prod::config::Config;
use zero2prod::email::{EmailSender, EmailService};

pub struct MockEmailSender {
pub sent_messages: Arc<Mutex<Vec<Message>>>,
}

impl MockEmailSender {
fn new() -> Self {
Self {
sent_messages: Arc::new(Mutex::new(Vec::new())),
}
}
}

impl EmailSender for MockEmailSender {
fn send(
&self,
_port: u16,
_host: &str,
_creds: lettre::transport::smtp::authentication::Credentials,
message: lettre::Message,
) -> Result<()> {
self.sent_messages.lock().unwrap().push(message);
Ok(())
}
}

pub struct TestApp {
address: String,
Expand Down Expand Up @@ -55,7 +84,10 @@ impl TestApp {
pub async fn spawn_app() -> Result<TestApp> {
let config = Config::new();

let app = Application::build(&config, "127.0.0.1:0".into()).await?;
static EMAIL_SENDER: Lazy<MockEmailSender> = Lazy::new(|| MockEmailSender::new());
let email_service = EmailService::new(config.smtp_config.clone(), &*EMAIL_SENDER);

let app = Application::build(&config, "127.0.0.1:0".into(), email_service).await?;
let address = format!("http://127.0.0.1:{}", app.port());
let _ = tokio::spawn(app.run_until_stopped());

Expand Down

0 comments on commit f7bb66f

Please sign in to comment.