diff --git a/configuration/base.yaml b/configuration/base.yaml index bdd290c..2a4ad1a 100644 --- a/configuration/base.yaml +++ b/configuration/base.yaml @@ -10,3 +10,4 @@ email_client: base_url: "localhost" sender_email: "test@fmail.com" authorization_token: "my-secret-token" + timeout_milliseconds: 10000 diff --git a/src/configuration.rs b/src/configuration.rs index 98f7a23..bdfe58f 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -22,12 +22,16 @@ pub struct EmailClientSettings { pub base_url: String, pub sender_email: String, pub authorization_token: Secret, + pub timeout_milliseconds: u64, } impl EmailClientSettings { pub fn sender(&self) -> Result { SubscriberEmail::parse(self.sender_email.clone()) } + pub fn timeout(&self) -> std::time::Duration { + std::time::Duration::from_millis(self.timeout_milliseconds) + } } #[derive(serde::Deserialize)] diff --git a/src/email_client.rs b/src/email_client.rs index 87c45e8..b85336e 100644 --- a/src/email_client.rs +++ b/src/email_client.rs @@ -15,9 +15,11 @@ impl EmailClient { base_url: String, sender: SubscriberEmail, authorization_token: Secret, + timeout: std::time::Duration, ) -> Self { + let http_client = Client::builder().timeout(timeout).build().unwrap(); Self { - http_client: Client::new(), + http_client, base_url, sender, authorization_token, @@ -98,38 +100,33 @@ mod tests { } } - #[tokio::test] - async fn send_email_succeeds_if_the_server_returns_200() { - // 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())); - - let subscriber_email = SubscriberEmail::parse(SafeEmail().fake()).unwrap(); - let subject: String = Sentence(1..2).fake(); - let content: String = Paragraph(1..10).fake(); - - Mock::given(any()) - .respond_with(ResponseTemplate::new(200)) - .expect(1) - .mount(&mock_server) - .await; - - // Act - let outcome = email_client - .send_email(subscriber_email, &subject, &content, &content) - .await; - - // Assert - assert_ok!(outcome); + /// Generate a random email subject + fn subject() -> String { + Sentence(1..2).fake() + } + /// Generate a random email content + fn content() -> String { + Paragraph(1..10).fake() + } + /// Generate a random subscriber email + fn email() -> SubscriberEmail { + SubscriberEmail::parse(SafeEmail().fake()).unwrap() + } + /// Get a test instance of `EmailClient`. + fn email_client(base_url: String) -> EmailClient { + EmailClient::new( + base_url, + email(), + Secret::new(Faker.fake()), + std::time::Duration::from_millis(200), + ) } #[tokio::test] async fn send_email_sends_the_expected_request() { // 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())); + let email_client = email_client(mock_server.uri()); Mock::given(header_exists("X-Postmark-Server-Token")) .and(header("Content-Type", "application/json")) @@ -141,29 +138,41 @@ mod tests { .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) + .send_email(email(), &subject(), &content(), &content()) .await; // Assert // Mock expectations are checked on drop } + #[tokio::test] + async fn send_email_succeeds_if_the_server_returns_200() { + // Arrange + let mock_server = MockServer::start().await; + let email_client = email_client(mock_server.uri()); + + Mock::given(any()) + .respond_with(ResponseTemplate::new(200)) + .expect(1) + .mount(&mock_server) + .await; + + // Act + let outcome = email_client + .send_email(email(), &subject(), &content(), &content()) + .await; + + // Assert + assert_ok!(outcome); + } + #[tokio::test] async fn send_email_fails_if_the_server_returns_500() { // 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())); - - let subscriber_email = SubscriberEmail::parse(SafeEmail().fake()).unwrap(); - let subject: String = Sentence(1..2).fake(); - let content: String = Paragraph(1..10).fake(); + let email_client = email_client(mock_server.uri()); Mock::given(any()) // Not a 200 anymore! @@ -174,7 +183,31 @@ mod tests { // Act let outcome = email_client - .send_email(subscriber_email, &subject, &content, &content) + .send_email(email(), &subject(), &content(), &content()) + .await; + + // Assert + assert_err!(outcome); + } + + #[tokio::test] + async fn send_email_times_out_if_the_server_takes_too_long() { + // Arrange + let mock_server = MockServer::start().await; + let email_client = email_client(mock_server.uri()); + + let response = ResponseTemplate::new(200) + // 3 minutes! + .set_delay(std::time::Duration::from_secs(180)); + Mock::given(any()) + .respond_with(response) + .expect(1) + .mount(&mock_server) + .await; + + // Act + let outcome = email_client + .send_email(email(), &subject(), &content(), &content()) .await; // Assert diff --git a/src/main.rs b/src/main.rs index b479757..388e19b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,10 +25,12 @@ async fn main() { .email_client .sender() .expect("Invalid sender email address."); + let timeout = configuration.email_client.timeout(); let email_client = EmailClient::new( configuration.email_client.base_url, sender_email, configuration.email_client.authorization_token, + timeout, ); startup::run(listener, connection_pool, email_client) .await diff --git a/tests/health_check.rs b/tests/health_check.rs index a02064f..bc6440f 100644 --- a/tests/health_check.rs +++ b/tests/health_check.rs @@ -153,10 +153,12 @@ async fn spawn_app() -> TestApp { .email_client .sender() .expect("Invalid sender email address."); + let timeout = configuration.email_client.timeout(); let email_client = EmailClient::new( configuration.email_client.base_url, sender_email, configuration.email_client.authorization_token, + timeout, ); let service = learn_axum::startup::app(connection_pool.clone(), email_client);