Skip to content

Commit ad112d4

Browse files
committed
feat: added faster timeout for client
1 parent c3ee6f7 commit ad112d4

File tree

5 files changed

+130
-11
lines changed

5 files changed

+130
-11
lines changed

configuration/base.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ database:
1111
email_client:
1212
base_url: "localhost"
1313
sender_email: "[email protected]"
14-
authorization_token: "my-secret-token"
14+
authorization_token: "my-secret-token"
15+
timeout_milliseconds: 10000

src/configuration.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,17 @@ pub struct EmailClientSettings {
2424
pub base_url: String,
2525
pub sender_email: String,
2626
pub authorization_token: Secret<String>,
27+
pub timeout_ms: u64,
2728
}
2829

2930
impl EmailClientSettings {
3031
pub fn sender(&self) -> Result<SubscriberEmail, String> {
3132
SubscriberEmail::parse(self.sender_email.clone())
3233
}
34+
35+
pub fn timeout(&self) -> std::time::Duration {
36+
std::time::Duration::from_millis(self.timeout_ms)
37+
}
3338
}
3439

3540
#[derive(serde::Deserialize)]

src/email_client.rs

Lines changed: 117 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,14 @@ impl EmailClient {
1515
base_url: String,
1616
sender: SubscriberEmail,
1717
authorization_token: Secret<String>,
18+
timeout: std::time::Duration,
1819
) -> Self {
20+
let http_client = Client::builder()
21+
.timeout(timeout)
22+
.build()
23+
.unwrap();
1924
Self {
20-
http_client: Client::new(),
25+
http_client,
2126
base_url,
2227
sender,
2328
authorization_token,
@@ -51,14 +56,15 @@ impl EmailClient {
5156
)
5257
.json(&request_body)
5358
.send()
54-
.await?;
59+
.await?
60+
.error_for_status()?;
5561
Ok(())
5662
}
5763
}
5864

5965
#[derive(serde::Serialize)]
6066
#[serde(rename_all = "PascalCase")]
61-
pub struct SendEmailRequest <'a> {
67+
pub struct SendEmailRequest<'a> {
6268
pub from: &'a str,
6369
pub to: &'a str,
6470
pub subject: &'a str,
@@ -69,14 +75,15 @@ pub struct SendEmailRequest <'a> {
6975
#[cfg(test)]
7076
mod tests {
7177
use crate::{domain::SubscriberEmail, email_client::EmailClient};
78+
use claim::{assert_err, assert_ok};
7279
use fake::{
7380
faker::internet::en::SafeEmail,
7481
faker::lorem::en::{Paragraph, Sentence},
7582
Fake, Faker,
7683
};
7784
use secrecy::Secret;
7885
use wiremock::{
79-
matchers::{header, header_exists, method, path},
86+
matchers::{any, header, header_exists, method, path},
8087
Mock, MockServer, Request, ResponseTemplate,
8188
};
8289

@@ -98,11 +105,31 @@ mod tests {
98105
}
99106
}
100107

108+
fn generate_subject() -> String {
109+
Sentence(1..2).fake()
110+
}
111+
112+
fn generate_content() -> String {
113+
Paragraph(1..10).fake()
114+
}
115+
116+
fn get_email_client(base_url: String) -> EmailClient {
117+
EmailClient::new(
118+
base_url,
119+
SubscriberEmail::parse(SafeEmail().fake()).unwrap(),
120+
Secret::new(Faker.fake()),
121+
std::time::Duration::from_millis(200),
122+
)
123+
}
124+
125+
fn get_email() -> SubscriberEmail {
126+
SubscriberEmail::parse(SafeEmail().fake()).unwrap()
127+
}
128+
101129
#[tokio::test]
102130
async fn send_email_sends_the_expected_request() {
103131
let mock_server = MockServer::start().await;
104-
let sender = SubscriberEmail::parse(SafeEmail().fake()).unwrap();
105-
let email_client = EmailClient::new(mock_server.uri(), sender, Secret::new(Faker.fake()));
132+
let email_client = get_email_client(mock_server.uri());
106133

107134
Mock::given(header_exists("X-Postmark-Server-Token"))
108135
.and(header("Content-Type", "application/json"))
@@ -114,12 +141,92 @@ mod tests {
114141
.mount(&mock_server)
115142
.await;
116143

117-
let recipient = SubscriberEmail::parse(SafeEmail().fake()).unwrap();
118-
let subject: String = Sentence(1..2).fake();
119-
let content: String = Paragraph(1..10).fake();
144+
let recipient = get_email();
120145

121146
let _ = email_client
122-
.send_email(recipient, &subject, &content, &content)
147+
.send_email(
148+
recipient,
149+
&generate_subject(),
150+
&generate_content(),
151+
&generate_content(),
152+
)
153+
.await;
154+
}
155+
156+
#[tokio::test]
157+
async fn send_email_succeeds_if_server_returns_200() {
158+
let mock_server = MockServer::start().await;
159+
let email_client = get_email_client(mock_server.uri());
160+
161+
let subscriber_email = get_email();
162+
163+
Mock::given(any())
164+
.respond_with(ResponseTemplate::new(200))
165+
.expect(1)
166+
.mount(&mock_server)
123167
.await;
168+
169+
let outcome = email_client
170+
.send_email(
171+
subscriber_email,
172+
&generate_subject(),
173+
&generate_content(),
174+
&generate_content(),
175+
)
176+
.await;
177+
178+
assert_ok!(outcome);
179+
}
180+
181+
#[tokio::test]
182+
async fn send_email_fails_if_server_returns_500() {
183+
let mock_server = MockServer::start().await;
184+
let email_client = get_email_client(mock_server.uri());
185+
186+
let subscriber_email = get_email();
187+
188+
Mock::given(any())
189+
.respond_with(ResponseTemplate::new(500))
190+
.expect(1)
191+
.mount(&mock_server)
192+
.await;
193+
194+
let outcome = email_client
195+
.send_email(
196+
subscriber_email,
197+
&generate_subject(),
198+
&generate_content(),
199+
&generate_content(),
200+
)
201+
.await;
202+
203+
assert_err!(outcome);
204+
}
205+
206+
#[tokio::test]
207+
async fn send_email_times_out_if_server_takes_too_long() {
208+
let mock_server = MockServer::start().await;
209+
let email_client = get_email_client(mock_server.uri());
210+
211+
let subscriber_email = get_email();
212+
213+
let response = ResponseTemplate::new(200).set_delay(std::time::Duration::from_secs(180));
214+
215+
Mock::given(any())
216+
.respond_with(response)
217+
.expect(1)
218+
.mount(&mock_server)
219+
.await;
220+
221+
let outcome = email_client
222+
.send_email(
223+
subscriber_email,
224+
&generate_subject(),
225+
&generate_content(),
226+
&generate_content(),
227+
)
228+
.await;
229+
230+
assert_err!(outcome);
124231
}
125232
}

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ async fn main() -> std::io::Result<()> {
2323
.sender()
2424
.expect("Invalid sender email address.");
2525

26+
let timeout = configuration.email_client.timeout();
2627
let email_client = EmailClient::new(
2728
configuration.email_client.base_url,
2829
sender_email,
2930
configuration.email_client.authorization_token,
31+
timeout
3032
);
3133

3234
let listener = TcpListener::bind(address).expect("Failed to bind random port");

tests/health_check.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,14 @@ async fn spawn_app() -> TestApp {
5050
.sender()
5151
.expect("Invalid sender email address.");
5252

53+
54+
let timeout = configuration.email_client.timeout();
55+
5356
let email_client = EmailClient::new(
5457
configuration.email_client.base_url,
5558
sender_email,
5659
configuration.email_client.authorization_token,
60+
timeout,
5761
);
5862

5963
// Launch our application as a background task

0 commit comments

Comments
 (0)