Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 00d24f8

Browse files
chore: Started working on idempotency
1 parent 08d1c03 commit 00d24f8

File tree

7 files changed

+58
-20
lines changed

7 files changed

+58
-20
lines changed

‎src/routes/admin/dashboard.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub async fn admin_dashboard(
3333
<p>Available actions:</p>
3434
<ol>
3535
<li><a href="/admin/password">Change password</a></li>
36+
<li><a href="/admin/newsletters">Send Newsletter</a></li>
3637
<li>
3738
<form name="logoutForm" action="/admin/logout" method="post">
3839
<input type="submit" value="Logout">

‎src/routes/admin/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
mod dashboard;
22
mod logout;
3-
mod password;
43
mod newsletters;
4+
mod password;
55

66
pub use dashboard::admin_dashboard;
77
pub use logout::log_out;
8-
pub use password::*;
98
pub use newsletters::*;
9+
pub use password::*;

‎src/routes/admin/newsletters/get.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ use actix_web::HttpResponse;
33
use actix_web_flash_messages::IncomingFlashMessages;
44
use std::fmt::Write;
55

6-
pub async fn publish_newsletter_form(flash_messages: IncomingFlashMessages) -> Result<HttpResponse, actix_web::Error> {
6+
pub async fn publish_newsletter_form(
7+
flash_messages: IncomingFlashMessages,
8+
) -> Result<HttpResponse, actix_web::Error> {
79
let mut incoming_flash = String::new();
810
for message in flash_messages.iter() {
911
writeln!(incoming_flash, "<p><i>{}</i></p>", message.content()).unwrap();
1012
}
1113
Ok(HttpResponse::Ok()
1214
.content_type(ContentType::html())
1315
.body(format!(
14-
r#"<!DOCTYPE html>
16+
r#"<!DOCTYPE html>
1517
<html lang="en">
1618
<head>
1719
<meta http-equiv="content-type" content="text/html; charset=utf-8">
@@ -52,4 +54,4 @@ pub async fn publish_newsletter_form(flash_messages: IncomingFlashMessages) -> R
5254
</body>
5355
</html>"#,
5456
)))
55-
}
57+
}

‎src/routes/admin/newsletters/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ mod get;
22
mod post;
33

44
pub use get::*;
5-
pub use post::*;
5+
pub use post::*;

‎src/routes/admin/newsletters/post.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,8 @@ use sqlx::PgPool;
1111
#[derive(serde::Deserialize)]
1212
pub struct BodyData {
1313
title: String,
14-
content: Content,
15-
}
16-
17-
#[derive(serde::Deserialize)]
18-
pub struct Content {
19-
html: String,
20-
text: String,
14+
text_content: String,
15+
html_content: String,
2116
}
2217

2318
#[tracing::instrument(
@@ -39,14 +34,16 @@ pub async fn publish_newsletter(
3934
.send_email(
4035
&subscriber.email,
4136
&body.title,
42-
&body.content.html,
43-
&body.content.text,
37+
&body.html_content,
38+
&body.text_content,
4439
)
4540
.await
4641
.with_context(|| {
4742
format!("Failed to send newsletter issue to {}", subscriber.email)
48-
}).map_err(e500)?;
43+
})
44+
.map_err(e500)?;
4945
}
46+
5047
Err(error) => {
5148
tracing::warn!(
5249
error.cause_chain = ?error,

‎src/startup.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@ use crate::{
44
email_client::EmailClient,
55
routes::{
66
admin_dashboard, change_password, change_password_form, confirm, health_check, home,
7-
log_out, login, login_form, publish_newsletter, subscribe,
7+
log_out, login, login_form, publish_newsletter, publish_newsletter_form,subscribe,
88
},
99
};
1010
use actix_session::storage::RedisSessionStore;
1111
use actix_session::SessionMiddleware;
1212
use actix_web::{cookie::Key, dev::Server, web, App, HttpServer};
1313
use actix_web_flash_messages::storage::CookieMessageStore;
1414
use actix_web_flash_messages::FlashMessagesFramework;
15+
use actix_web_lab::middleware::from_fn;
1516
use secrecy::{ExposeSecret, Secret};
1617
use sqlx::postgres::PgPoolOptions;
1718
use sqlx::PgPool;
1819
use std::net::TcpListener;
1920
use tracing_actix_web::TracingLogger;
20-
use actix_web_lab::middleware::from_fn;
2121

2222
pub struct Application {
2323
port: u16,
@@ -104,7 +104,6 @@ async fn run(
104104
.route("/health_check", web::get().to(health_check))
105105
.route("/subscriptions", web::post().to(subscribe))
106106
.route("/subscriptions/confirm", web::get().to(confirm))
107-
.route("/newsletters", web::post().to(publish_newsletter))
108107
.route("/", web::get().to(home))
109108
.route("/login", web::get().to(login_form))
110109
.route("/login", web::post().to(login))
@@ -114,7 +113,9 @@ async fn run(
114113
.route("/dashboard", web::get().to(admin_dashboard))
115114
.route("/password", web::get().to(change_password_form))
116115
.route("/password", web::post().to(change_password))
117-
.route("/logout", web::post().to(log_out)),
116+
.route("/logout", web::post().to(log_out))
117+
.route("/newsletters", web::post().to(publish_newsletter))
118+
.route("/newsletters", web::get().to(publish_newsletter_form)),
118119
)
119120
.app_data(db_pool.clone())
120121
.app_data(email_client.clone())

‎tests/api/newsletter.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,40 @@ async fn invalid_password_is_rejected() {
205205
response.headers()["WWW-Authenticate"]
206206
);
207207
}
208+
209+
#[tokio::test]
210+
async fn newsletter_creation_is_idempotent() {
211+
// Arrange
212+
let app = spawn_app().await;
213+
create_confirmed_subscriber(&app).await;
214+
app.test_user.login(&app).await;
215+
216+
// We create a mock server that will intercept the request
217+
Mock::given(path("/email"))
218+
.and(method("POST"))
219+
.respond_with(ResponseTemplate::new(200))
220+
.expect(1)
221+
.mount(&app.email_server)
222+
.await;
223+
// Act - Part 1 - Submit newsletter form
224+
let newsletter_request_body = serde_json::json!({
225+
"title": "Newsletter title",
226+
"text_content": "Newsletter body as plain text",
227+
"html_content": "<p>Newsletter body as HTML</p>",
228+
// We expect the idempotency key as part of the
229+
// form data, not as an header
230+
"idempotency_key": uuid::Uuid::new_v4().to_string()
231+
});
232+
let response = app.post_publish_newsletter(&newsletter_request_body).await;
233+
assert_is_redirect_to(&response, "/admin/newsletters");
234+
// Act - Part 2 - Follow the redirect
235+
let html_page = app.get_publish_newsletter_html().await;
236+
assert!(html_page.contains("<p><i>The newsletter issue has been published!</i></p>"));
237+
// Act - Part 3 - Submit newsletter form **again**
238+
let response = app.post_publish_newsletter(&newsletter_request_body).await;
239+
assert_is_redirect_to(&response, "/admin/newsletters");
240+
// Act - Part 4 - Follow the redirect
241+
let html_page = app.get_publish_newsletter_html().await;
242+
assert!(html_page.contains("<p><i>The newsletter issue has been published!</i></p>"));
243+
// Mock verifies on Drop that we have sent the newsletter email **once**
244+
}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /