From 0ccedba41149cd1534ea9607b8e2d9e810ad6973 Mon Sep 17 00:00:00 2001 From: Piotr Orzechowski Date: Fri, 12 Apr 2024 13:26:19 +0200 Subject: [PATCH] Add newsletter form endpoint --- src/routes/admin/dashboard.rs | 3 + src/routes/admin/mod.rs | 3 +- src/routes/admin/newsletters/get.rs | 36 ++++++++++++ src/routes/admin/newsletters/mod.rs | 2 + src/routes/admin/newsletters/post.rs | 27 ++++----- templates/web/change_password_form.html | 6 +- templates/web/dashboard.html | 1 + templates/web/newsletter_form.html | 32 +++++++++++ tests/api/admin_newsletters.rs | 76 +++++++++++++------------ tests/api/helpers.rs | 9 +-- 10 files changed, 136 insertions(+), 59 deletions(-) create mode 100644 src/routes/admin/newsletters/get.rs create mode 100644 templates/web/newsletter_form.html diff --git a/src/routes/admin/dashboard.rs b/src/routes/admin/dashboard.rs index e62b8d8..e2bbbd1 100644 --- a/src/routes/admin/dashboard.rs +++ b/src/routes/admin/dashboard.rs @@ -3,6 +3,7 @@ use crate::{ authentication::extract::SessionUserId, utils::{e500, HttpError}, }; + use anyhow::{Context, Error}; use askama::Template; use axum::extract::State; @@ -22,6 +23,7 @@ pub(super) async fn admin_dashboard( title: "Admin Dashboard", welcome: "Welcome", available_actions: "Available actions", + send_newsletter: "Send newsletter", change_password: "Change password", logout: "Logout", username, @@ -51,6 +53,7 @@ pub struct Dashboard<'a> { title: &'a str, welcome: &'a str, available_actions: &'a str, + send_newsletter: &'a str, change_password: &'a str, logout: &'a str, username: String, diff --git a/src/routes/admin/mod.rs b/src/routes/admin/mod.rs index bc67115..02bcb11 100644 --- a/src/routes/admin/mod.rs +++ b/src/routes/admin/mod.rs @@ -5,7 +5,7 @@ use axum::{ }; use dashboard::admin_dashboard; use logout::log_out; -use newsletters::publish_newsletter; +use newsletters::{newsletter_form, publish_newsletter}; use password::{change_password, change_password_form}; mod dashboard; @@ -19,6 +19,7 @@ pub fn router() -> Router { "/admin", Router::new() .route("/dashboard", get(admin_dashboard)) + .route("/newsletters", get(newsletter_form)) .route("/newsletters", post(publish_newsletter)) .route("/password", get(change_password_form)) .route("/password", post(change_password)) diff --git a/src/routes/admin/newsletters/get.rs b/src/routes/admin/newsletters/get.rs new file mode 100644 index 0000000..08f1b54 --- /dev/null +++ b/src/routes/admin/newsletters/get.rs @@ -0,0 +1,36 @@ +use askama_axum::Template; +use axum_messages::Messages; + +pub(in crate::routes::admin) async fn newsletter_form( + messages: Messages, +) -> NewsletterForm<'static> { + let flashes = messages.map(|m| m.message).collect(); + + NewsletterForm { + title: "Send Newsletter", + newsletter_title_label: "Newsletter title", + newsletter_title_placeholder: "Enter newsletter title", + newsletter_html_label: "Newsletter HTML content", + newsletter_html_placeholder: "Enter newsletter HTML content", + newsletter_text_label: "Newsletter text", + newsletter_text_placeholder: "Enter newsletter text", + send_newsletter_button: "Send newsletter", + back_link: "Back", + flashes, + } +} + +#[derive(Template)] +#[template(path = "web/newsletter_form.html")] +pub(in crate::routes::admin) struct NewsletterForm<'a> { + title: &'a str, + newsletter_title_label: &'a str, + newsletter_title_placeholder: &'a str, + newsletter_html_label: &'a str, + newsletter_html_placeholder: &'a str, + newsletter_text_label: &'a str, + newsletter_text_placeholder: &'a str, + send_newsletter_button: &'a str, + back_link: &'a str, + flashes: Vec, +} diff --git a/src/routes/admin/newsletters/mod.rs b/src/routes/admin/newsletters/mod.rs index 6991fe3..56d747e 100644 --- a/src/routes/admin/newsletters/mod.rs +++ b/src/routes/admin/newsletters/mod.rs @@ -1,3 +1,5 @@ +mod get; mod post; +pub(super) use get::newsletter_form; pub(super) use post::publish_newsletter; diff --git a/src/routes/admin/newsletters/post.rs b/src/routes/admin/newsletters/post.rs index 1c4a018..eeda95a 100644 --- a/src/routes/admin/newsletters/post.rs +++ b/src/routes/admin/newsletters/post.rs @@ -4,14 +4,14 @@ use crate::{ utils::{e500, HttpError}, }; use anyhow::Context; -use axum::{extract::State, Json}; +use axum::{extract::State, Form}; use serde::Deserialize; use sqlx::PgPool; -#[tracing::instrument(name = "Publish newsletter", skip(app_state, body))] +#[tracing::instrument(skip(app_state, form))] pub(in crate::routes::admin) async fn publish_newsletter( State(app_state): State, - Json(body): Json, + Form(form): Form, ) -> Result<(), HttpError> { for subscriber in get_confirmed_subscribers(&app_state.db_pool) .await @@ -22,9 +22,9 @@ pub(in crate::routes::admin) async fn publish_newsletter( .email_client .send_email( &subscriber.email, - &body.title, - &body.content.html, - &body.content.text, + &form.newsletter_title, + &form.newsletter_html, + &form.newsletter_text, ) .await .with_context(|| { @@ -41,7 +41,7 @@ pub(in crate::routes::admin) async fn publish_newsletter( Ok(()) } -#[tracing::instrument(name = "Get confirmed subscribers", skip(db_pool))] +#[tracing::instrument(skip(db_pool))] async fn get_confirmed_subscribers( db_pool: &PgPool, ) -> Result>, anyhow::Error> { @@ -68,15 +68,10 @@ async fn get_confirmed_subscribers( } #[derive(Deserialize)] -pub(in crate::routes::admin) struct BodyData { - title: String, - content: Content, -} - -#[derive(Deserialize)] -struct Content { - html: String, - text: String, +pub(in crate::routes::admin) struct FormData { + newsletter_title: String, + newsletter_html: String, + newsletter_text: String, } struct ConfirmedSubscriber { diff --git a/templates/web/change_password_form.html b/templates/web/change_password_form.html index 13801b4..0fb81a7 100644 --- a/templates/web/change_password_form.html +++ b/templates/web/change_password_form.html @@ -8,17 +8,17 @@



diff --git a/templates/web/dashboard.html b/templates/web/dashboard.html index 82a479b..70e1314 100644 --- a/templates/web/dashboard.html +++ b/templates/web/dashboard.html @@ -4,6 +4,7 @@

{{ welcome }}, {{ username }}!

{{ available_actions }}:

    +
  1. {{ send_newsletter }}
  2. {{ change_password }}
  3. diff --git a/templates/web/newsletter_form.html b/templates/web/newsletter_form.html new file mode 100644 index 0000000..70c34d0 --- /dev/null +++ b/templates/web/newsletter_form.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} + +{% block content %} +{%- for flash in flashes %} +

    {{ flash }}

    +{%- endfor %} + + + +
    +
    + +
    +
    + +
    +
    + +
  4. +

    <- {{ back_link }}

    +{% endblock %} diff --git a/tests/api/admin_newsletters.rs b/tests/api/admin_newsletters.rs index 93f61b1..e04f13c 100644 --- a/tests/api/admin_newsletters.rs +++ b/tests/api/admin_newsletters.rs @@ -9,13 +9,12 @@ use wiremock::{ async fn newsletters_are_delivered_to_confirmed_subscribers() { // given let app = TestApp::spawn().await; - let newsletter_request_body = json!({ - "title": "Newsletter Title", - "content": { - "text": "Newsletter body as plain text.", - "html": "

    Newsletter body as html.

    ", - } - }); + let newsletter_request_body = "\ + newsletter_title=Newsletter%20Title&\ + newsletter_html=%3Cp%3ENewsletter%20body%20as%20html.%3C%2Fp%3E&\ + newsletter_text=%3Cp%3ENewsletter%20body%20as%20html.%3C%2Fp%3E\ + " + .to_string(); create_confirmed_subscriber(&app).await; Mock::given(path("/email")) @@ -34,7 +33,7 @@ async fn newsletters_are_delivered_to_confirmed_subscribers() { assert_redirect_to(&response, "/admin/dashboard"); // when - let response = app.post_newsletters(&newsletter_request_body).await; + let response = app.post_newsletters(newsletter_request_body).await; // then assert_eq!(response.status(), 200); @@ -44,13 +43,12 @@ async fn newsletters_are_delivered_to_confirmed_subscribers() { async fn newsletters_are_not_delivered_to_unconfirmed_subscribers() { // given let app = TestApp::spawn().await; - let newsletter_request_body = json!({ - "title": "Newsletter Title", - "content": { - "text": "Newsletter body as plain text.", - "html": "

    Newsletter body as html.

    ", - } - }); + let newsletter_request_body = "\ + newsletter_title=Newsletter%20Title&\ + newsletter_html=%3Cp%3ENewsletter%20body%20as%20html.%3C%2Fp%3E&\ + newsletter_text=%3Cp%3ENewsletter%20body%20as%20html.%3C%2Fp%3E\ + " + .to_string(); create_unfonfirmed_subscriber(&app).await; Mock::given(any()) @@ -68,7 +66,7 @@ async fn newsletters_are_not_delivered_to_unconfirmed_subscribers() { assert_redirect_to(&response, "/admin/dashboard"); // when - let response = app.post_newsletters(&newsletter_request_body).await; + let response = app.post_newsletters(newsletter_request_body).await; // then assert_eq!(response.status(), 200); @@ -80,19 +78,28 @@ async fn newsletters_returns_400_for_invalid_data() { let app = TestApp::spawn().await; let test_cases = vec![ ( - json!({ - "content": { - "text": "Newsletter body as plain text.", - "html": "

    Newsletter body as html.

    ", - } - }), + "\ + newsletter_html=%3Cp%3ENewsletter%20body%20as%20html.%3C%2Fp%3E&\ + newsletter_text=%3Cp%3ENewsletter%20body%20as%20html.%3C%2Fp%3E\ + " + .to_string(), "missing title", ), ( - json!({ - "title": "Newsletter Title", - }), - "missing content", + "\ + newsletter_title=Newsletter%20Title&\ + newsletter_text=%3Cp%3ENewsletter%20body%20as%20html.%3C%2Fp%3E\ + " + .to_string(), + "missing html content", + ), + ( + "\ + newsletter_title=Newsletter%20Title&\ + newsletter_html=%3Cp%3ENewsletter%20body%20as%20html.%3C%2Fp%3E\ + " + .to_string(), + "missing text content", ), ]; @@ -106,7 +113,7 @@ async fn newsletters_returns_400_for_invalid_data() { for (body, error_message) in test_cases { // when - let response = app.post_newsletters(&body).await; + let response = app.post_newsletters(body).await; // then assert_eq!( @@ -122,16 +129,15 @@ async fn newsletters_returns_400_for_invalid_data() { async fn requests_from_anonymous_users_are_redirected_to_login() { // given let app = TestApp::spawn().await; - let newsletter_request_body = json!({ - "title": "Newsletter Title", - "content": { - "text": "Newsletter body as plain text.", - "html": "

    Newsletter body as html.

    ", - } - }); + let newsletter_request_body = "\ + newsletter_title=Newsletter%20Title&\ + newsletter_html=%3Cp%3ENewsletter%20body%20as%20html.%3C%2Fp%3E&\ + newsletter_text=%3Cp%3ENewsletter%20body%20as%20html.%3C%2Fp%3E\ + " + .to_string(); // when - let response = app.post_newsletters(&newsletter_request_body).await; + let response = app.post_newsletters(newsletter_request_body).await; // then assert_redirect_to(&response, "/login"); diff --git a/tests/api/helpers.rs b/tests/api/helpers.rs index 5c4c9ea..59ecb2e 100644 --- a/tests/api/helpers.rs +++ b/tests/api/helpers.rs @@ -2,7 +2,7 @@ use argon2::{password_hash::SaltString, Algorithm, Argon2, Params, PasswordHashe use claims::assert_some_eq; use linkify::{LinkFinder, LinkKind}; use once_cell::sync::Lazy; -use reqwest::{redirect, Response}; +use reqwest::{header::CONTENT_TYPE, redirect, Response}; use serde::Serialize; use sqlx::{Connection, Executor, PgConnection, PgPool}; use std::{net::SocketAddr, str::FromStr}; @@ -102,7 +102,7 @@ impl TestApp { pub async fn post_subscriptions(&self, body: String) -> reqwest::Response { self.client .post(self.url("/subscriptions")) - .header("Content-Type", "application/x-www-form-urlencoded") + .header(CONTENT_TYPE, "application/x-www-form-urlencoded") .body(body) .send() .await @@ -168,10 +168,11 @@ impl TestApp { self.get_admin_dashboard().await.text().await.unwrap() } - pub async fn post_newsletters(&self, body: &serde_json::Value) -> reqwest::Response { + pub async fn post_newsletters(&self, body: String) -> reqwest::Response { self.client .post(self.url("/admin/newsletters")) - .json(body) + .header(CONTENT_TYPE, "application/x-www-form-urlencoded") + .body(body) .send() .await .expect(Self::FAILED_TO_EXECUTE_REQUEST)