Skip to content

Commit ca45d1f

Browse files
committed
11.11
1 parent 3005269 commit ca45d1f

5 files changed

Lines changed: 75 additions & 60 deletions

File tree

src/configuration.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
use crate::domain::SubscriberEmail;
2+
use crate::email_client::EmailClient;
23
use secrecy::ExposeSecret;
34
use secrecy::Secret;
45
use sqlx::postgres::{PgConnectOptions, PgSslMode};
56

67
use serde_aux::field_attributes::deserialize_number_from_string;
78
use std::convert::{TryFrom, TryInto};
89

10+
11+
impl EmailClientSettings {
12+
pub fn client(self) -> EmailClient {
13+
let sender_email = self.sender().expect("Invalid sender email address.");
14+
let timeout = self.timeout();
15+
EmailClient::new(
16+
self.base_url,
17+
sender_email,
18+
self.authorization_token,
19+
timeout,
20+
)
21+
}
22+
23+
}
24+
925
#[derive(serde::Deserialize, Clone)]
1026
pub struct Settings {
1127
pub database: DatabaseSettings,

src/issue_delivery_worker.rs

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use sqlx::{Executor, PgPool, Postgres, Transaction};
77
use tracing::{field::display, Span};
88
use uuid::Uuid;
99

10-
enum ExecutionOutcome {
10+
pub enum ExecutionOutcome {
1111
TaskCompleted,
1212
EmptyQueue,
1313
}
@@ -20,7 +20,7 @@ enum ExecutionOutcome {
2020
),
2121
err
2222
)]
23-
async fn try_execute_task(pool: &PgPool, email_client: &EmailClient) -> Result<ExecutionOutcome, anyhow::Error> {
23+
pub async fn try_execute_task(pool: &PgPool, email_client: &EmailClient) -> Result<ExecutionOutcome, anyhow::Error> {
2424
let task = dequeue_task(pool).await?;
2525
if task.is_none() {
2626
return Ok(ExecutionOutcome::EmptyQueue)
@@ -155,17 +155,6 @@ pub async fn run_worker_until_stopped(
155155
configuration: Settings
156156
) -> Result<(), anyhow::Error> {
157157
let connection_pool = get_connection_pool(&configuration.database);
158-
159-
let sender_email = configuration
160-
.email_client
161-
.sender()
162-
.expect("Invalid sender email address.");
163-
let timeout = configuration.email_client.timeout();
164-
let email_client = EmailClient::new(
165-
configuration.email_client.base_url,
166-
sender_email,
167-
configuration.email_client.authorization_token,
168-
timeout
169-
);
158+
let email_client = configuration.email_client.client();
170159
worker_loop(connection_pool, email_client).await
171160
}

src/startup.rs

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::authentication::reject_anonymous_users;
22
use crate::configuration::DatabaseSettings;
33
use crate::configuration::Settings;
4+
use crate::email_client;
45
use crate::email_client::EmailClient;
56
use crate::routes::admin_dashboard;
67
use crate::routes::{change_password, change_password_form};
@@ -36,19 +37,7 @@ impl Application {
3637
pub async fn build(configuration: Settings) -> Result<Self, std::io::Error> {
3738
let connection_pool = get_connection_pool(&configuration.database);
3839

39-
let sender_email = configuration
40-
.email_client
41-
.sender()
42-
.expect("Invalid sender email address.");
43-
44-
let timeout = configuration.email_client.timeout();
45-
46-
let email_client = EmailClient::new(
47-
configuration.email_client.base_url,
48-
sender_email,
49-
configuration.email_client.authorization_token,
50-
timeout,
51-
);
40+
let email_client = configuration.email_client.client();
5241
let address = format!(
5342
"{}:{}",
5443
configuration.application.host, configuration.application.port

tests/api/helpers.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use wiremock::MockServer;
1010
use zero2prod::configuration::{get_configuration, DatabaseSettings};
1111
use zero2prod::startup::{get_connection_pool, Application};
1212
use zero2prod::telemetry::{get_subscriber, init_subscriber};
13+
use zero2prod::email_client::EmailClient;
14+
use zero2prod::issue_delivery_worker::{try_execute_task, ExecutionOutcome};
1315

1416
static TRACING: LazyLock<()> = LazyLock::new(|| {
1517
let _default_filter_level = "info".to_string();
@@ -76,6 +78,7 @@ pub struct TestApp {
7678
pub port: u16,
7779
pub test_user: TestUser,
7880
pub api_client: reqwest::Client,
81+
pub email_client: EmailClient,
7982
}
8083

8184
pub struct ConfirmationLinks {
@@ -84,6 +87,17 @@ pub struct ConfirmationLinks {
8487
}
8588

8689
impl TestApp {
90+
pub async fn dispatch_all_pending_emails(&self) {
91+
loop {
92+
if let ExecutionOutcome::EmptyQueue = try_execute_task(&self.db_pool, &self.email_client)
93+
.await
94+
.unwrap()
95+
{
96+
break;
97+
}
98+
}
99+
}
100+
87101
pub async fn post_logout(&self) -> reqwest::Response {
88102
self.api_client
89103
.post(&format!("{}/admin/logout", &self.address))
@@ -259,6 +273,7 @@ pub async fn spawn_app() -> TestApp {
259273
email_server,
260274
test_user: TestUser::generate(),
261275
api_client: client,
276+
email_client: configuration.email_client.client(),
262277
};
263278
test_app.test_user.store(&test_app.db_pool).await;
264279
test_app

tests/api/newsletter.rs

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ async fn concurrent_form_submissions_handled_gracefully() {
3434
assert_eq!(
3535
response1.text().await.unwrap(),
3636
response2.text().await.unwrap()
37-
)
37+
);
38+
39+
app.dispatch_all_pending_emails().await;
3840
}
3941

4042
#[tokio::test]
@@ -132,7 +134,11 @@ async fn newsletters_are_delivered_to_confirmed_subscribers() {
132134
.await;
133135
assert_is_redirect_to(&response, "/admin/newsletters");
134136
let html_page = app.get_submit_newsletters_html().await;
135-
assert!(html_page.contains("<p><i>The newsletter issue has been published!</i></p>"));
137+
assert!(html_page.contains(
138+
"The newsletter issue has been accepted - \
139+
email will go out shortly."
140+
));
141+
app.dispatch_all_pending_emails().await;
136142
// Mock verifies on Drop that we have sent the newsletter email
137143
}
138144

@@ -160,8 +166,12 @@ async fn newsletters_are_not_delivered_to_unconfirmed_subscribers() {
160166
let response = app.post_newsletters(&newsletter_request_body).await;
161167
assert_is_redirect_to(&response, "/admin/newsletters");
162168
let html_page = app.get_submit_newsletters_html().await;
169+
assert!(html_page.contains(
170+
"The newsletter issue has been accepted - \
171+
email will go out shortly."
172+
));
173+
app.dispatch_all_pending_emails().await;
163174
// assert on injected FlashMessage content
164-
assert!(html_page.contains("<p><i>The newsletter issue has been published!</i></p>"));
165175
// Mock verifies on Drop that we haven't sent the newsletter email
166176
}
167177

@@ -237,47 +247,43 @@ async fn create_confirmed_subscriber(app: &TestApp) {
237247
.error_for_status()
238248
.unwrap();
239249
}
240-
241-
fn when_sending_an_email() -> MockBuilder {
242-
Mock::given(path("/email")).and(method("POST"))
243-
}
244-
245250
#[tokio::test]
246-
async fn transient_errors_do_nost_cause_duplicate_deliveries_on_retries() {
251+
async fn newsletter_creation_is_idempotent() {
252+
// Arrange
247253
let app = spawn_app().await;
248-
let newsletter_request_body = serde_json::json!({
249-
"title": "Newsletter title",
250-
"text_content": "Newsletter body as plain text",
251-
"html_content": "<p>Newsletter body as HTML</p>",
252-
"idempotency_key": uuid::Uuid::new_v4().to_string(),
253-
});
254-
create_confirmed_subscriber(&app).await;
255254
create_confirmed_subscriber(&app).await;
256255
app.test_user.login(&app).await;
257-
when_sending_an_email()
256+
257+
Mock::given(path("/email"))
258+
.and(method("POST"))
258259
.respond_with(ResponseTemplate::new(200))
259-
.up_to_n_times(1)
260-
.expect(1)
261-
.mount(&app.email_server)
262-
.await;
263-
// second delivery should fail
264-
when_sending_an_email()
265-
.respond_with(ResponseTemplate::new(500))
266-
.up_to_n_times(1)
267260
.expect(1)
268261
.mount(&app.email_server)
269262
.await;
270263

264+
let newsletter_request_body = serde_json::json!({
265+
"title": "Newsletter title",
266+
"text_content": "Newsletter body as plain text",
267+
"html_content": "<p>Newsletter body as HTML</p>",
268+
"idempotency_key": uuid::Uuid::new_v4().to_string()
269+
});
271270
let response = app.post_newsletters(&newsletter_request_body).await;
271+
assert_is_redirect_to(&response, "/admin/newsletters");
272272

273-
assert_eq!(response.status().as_u16(), 500);
274-
when_sending_an_email()
275-
.respond_with(ResponseTemplate::new(200))
276-
.expect(1)
277-
.named("Delivery retry")
278-
.mount(&app.email_server)
279-
.await;
273+
let html_page = app.get_submit_newsletters_html().await;
274+
assert!(html_page.contains(
275+
"<p><i>The newsletter issue has been accepted - \
276+
emails will go out shortly.</i></p>"
277+
));
280278

281279
let response = app.post_newsletters(&newsletter_request_body).await;
282-
assert_eq!(response.status().as_u16(), 303);
280+
assert_is_redirect_to(&response, "/admin/newsletters");
281+
282+
let html_page = app.get_submit_newsletters_html().await;
283+
assert!(html_page.contains(
284+
"The newsletter issue has been accepted - \
285+
email will go out shortly."
286+
));
287+
app.dispatch_all_pending_emails().await;
283288
}
289+

0 commit comments

Comments
 (0)