use crate::helpers::{assert_is_redirect_to, spawn_app, ConfirmationLinks, TestApp}; use fake::faker::internet::en::SafeEmail; use fake::faker::name::en::Name; use fake::Fake; use std::time::Duration; use wiremock::matchers::{any, method, path}; use wiremock::{Mock, MockBuilder, ResponseTemplate}; async fn create_unconfirmed_subscriber(app: &TestApp) -> ConfirmationLinks { // We are working with multiple subscribers now, // their details must be randomised to avoid conflicts! let name: String = Name().fake(); let email: String = SafeEmail().fake(); let body = serde_urlencoded::to_string(&serde_json::json!({ "name": name, "email": email })) .unwrap(); let _mock_guard = Mock::given(path("/email")) .and(method("POST")) .respond_with(ResponseTemplate::new(200)) .named("Create unconfirmed subscriber") .expect(1) .mount_as_scoped(&app.email_server) .await; app.post_subscriptions(body.into()) .await .error_for_status() .unwrap(); let email_request = &app .email_server .received_requests() .await .unwrap() .pop() .unwrap(); app.get_confirmation_links(email_request) } async fn create_confirmed_subscriber(app: &TestApp) { let confirmation_link = create_unconfirmed_subscriber(app).await.html; reqwest::get(confirmation_link) .await .unwrap() .error_for_status() .unwrap(); } #[tokio::test] async fn newsletters_are_not_delivered_to_unconfirmed_subscribers() { // Arrange let app = spawn_app().await; create_unconfirmed_subscriber(&app).await; app.test_user.login(&app).await; Mock::given(any()) .respond_with(ResponseTemplate::new(200)) .expect(0) .mount(&app.email_server) .await; // Act - Part 1 - Submit newsletter form let newsletter_request_body = serde_json::json!({ "title": "Newsletter title", "text_content": "Newsletter body as plain text", "html_content": "

Newsletter body as HTML

", "idempotency_key": uuid::Uuid::new_v4().to_string() }); let response = app.post_publish_newsletter(&newsletter_request_body).await; assert_is_redirect_to(&response, "/admin/newsletters"); // Act - Part 2 - Follow the redirect let html_page = app.get_publish_newsletter_html().await; assert!(html_page.contains( "

The newsletter issue has been accepted - \ emails will go out shortly.

" )); app.dispatch_all_pending_emails().await; // Mock verifies on Drop that we haven't sent the newsletter email } #[tokio::test] async fn newsletters_are_delivered_to_confirmed_subscribers() { // Arrange let app = spawn_app().await; create_confirmed_subscriber(&app).await; app.test_user.login(&app).await; Mock::given(path("/email")) .and(method("POST")) .respond_with(ResponseTemplate::new(200)) .expect(1) .mount(&app.email_server) .await; // Act - Part 1 - Submit newsletter form let newsletter_request_body = serde_json::json!({ "title": "Newsletter title", "text_content": "Newsletter body as plain text", "html_content": "

Newsletter body as HTML

", "idempotency_key": uuid::Uuid::new_v4().to_string() }); let response = app.post_publish_newsletter(&newsletter_request_body).await; assert_is_redirect_to(&response, "/admin/newsletters"); // Act - Part 2 - Follow the redirect let html_page = app.get_publish_newsletter_html().await; assert!(html_page.contains( "

The newsletter issue has been accepted - \ emails will go out shortly.

" )); app.dispatch_all_pending_emails().await; // Mock verifies on Drop that we have sent the newsletter email } #[tokio::test] async fn you_must_be_logged_in_to_see_the_newsletter_form() { // Arrange let app = spawn_app().await; // Act let response = app.get_publish_newsletter().await; // Assert assert_is_redirect_to(&response, "/login"); } #[tokio::test] async fn you_must_be_logged_in_to_publish_a_newsletter() { // Arrange let app = spawn_app().await; // Act let newsletter_request_body = serde_json::json!({ "title": "Newsletter title", "text_content": "Newsletter body as plain text", "html_content": "

Newsletter body as HTML

", "idempotency_key": uuid::Uuid::new_v4().to_string() }); let response = app.post_publish_newsletter(&newsletter_request_body).await; // Assert assert_is_redirect_to(&response, "/login"); } #[tokio::test] async fn newsletter_creation_is_idempotent() { // Arrange let app = spawn_app().await; create_confirmed_subscriber(&app).await; app.test_user.login(&app).await; Mock::given(path("/email")) .and(method("POST")) .respond_with(ResponseTemplate::new(200)) .expect(1) .mount(&app.email_server) .await; // Act - Part 1 - Submit newsletter form let newsletter_request_body = serde_json::json!({ "title": "Newsletter title", "text_content": "Newsletter body as plain text", "html_content": "

Newsletter body as HTML

", // We expect the idempotency key as part of the // form data, not as an header "idempotency_key": uuid::Uuid::new_v4().to_string() }); let response = app.post_publish_newsletter(&newsletter_request_body).await; assert_is_redirect_to(&response, "/admin/newsletters"); // Act - Part 2 - Follow the redirect let html_page = app.get_publish_newsletter_html().await; assert!(html_page.contains( "

The newsletter issue has been accepted - \ emails will go out shortly.

" )); // Act - Part 3 - Submit newsletter form **again** let response = app.post_publish_newsletter(&newsletter_request_body).await; assert_is_redirect_to(&response, "/admin/newsletters"); // Act - Part 4 - Follow the redirect let html_page = app.get_publish_newsletter_html().await; assert!(html_page.contains( "

The newsletter issue has been accepted - \ emails will go out shortly.

" )); app.dispatch_all_pending_emails().await; // Mock verifies on Drop that we have sent the newsletter email **once** } #[tokio::test] async fn concurrent_form_submission_is_handled_gracefully() { // Arrange let app = spawn_app().await; create_confirmed_subscriber(&app).await; app.test_user.login(&app).await; Mock::given(path("/email")) .and(method("POST")) // Setting a long delay to ensure that the second request // arrives before the first one completes .respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(2))) .expect(1) .mount(&app.email_server) .await; // Act - Submit two newsletter forms concurrently let newsletter_request_body = serde_json::json!({ "title": "Newsletter title", "text_content": "Newsletter body as plain text", "html_content": "

Newsletter body as HTML

", "idempotency_key": uuid::Uuid::new_v4().to_string() }); let response1 = app.post_publish_newsletter(&newsletter_request_body); let response2 = app.post_publish_newsletter(&newsletter_request_body); let (response1, response2) = tokio::join!(response1, response2); assert_eq!(response1.status(), response2.status()); assert_eq!( response1.text().await.unwrap(), response2.text().await.unwrap() ); app.dispatch_all_pending_emails().await; // Mock verifies on Drop that we have sent the newsletter email **once** }