Skip to content
This repository has been archived by the owner on Aug 21, 2024. It is now read-only.

Commit

Permalink
Add newsletter form endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
0rzech committed Apr 12, 2024
1 parent 5d93f19 commit 0ccedba
Show file tree
Hide file tree
Showing 10 changed files with 136 additions and 59 deletions.
3 changes: 3 additions & 0 deletions src/routes/admin/dashboard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::{
authentication::extract::SessionUserId,
utils::{e500, HttpError},
};

use anyhow::{Context, Error};
use askama::Template;
use axum::extract::State;
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/routes/admin/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +19,7 @@ pub fn router() -> Router<AppState> {
"/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))
Expand Down
36 changes: 36 additions & 0 deletions src/routes/admin/newsletters/get.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
}
2 changes: 2 additions & 0 deletions src/routes/admin/newsletters/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
mod get;
mod post;

pub(super) use get::newsletter_form;
pub(super) use post::publish_newsletter;
27 changes: 11 additions & 16 deletions src/routes/admin/newsletters/post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AppState>,
Json(body): Json<BodyData>,
Form(form): Form<FormData>,
) -> Result<(), HttpError<anyhow::Error>> {
for subscriber in get_confirmed_subscribers(&app_state.db_pool)
.await
Expand All @@ -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(|| {
Expand All @@ -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<Vec<Result<ConfirmedSubscriber, anyhow::Error>>, anyhow::Error> {
Expand All @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions templates/web/change_password_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
<form action="/admin/password" method="post">
<label>
{{ current_password_label }}
<input type="password" placeholder="{{ current_password_placeholder }}" name="current_password">
<input type="password" placeholder="{{ current_password_placeholder }}" name="current_password" required>
</label>
<br>
<label>
{{ new_password_label }}
<input type="password" placeholder="{{ new_password_placeholder }}" name="new_password">
<input type="password" placeholder="{{ new_password_placeholder }}" name="new_password" required>
</label>
<br>
<label>
{{ new_password_check_label }}
<input type="password" placeholder="{{ new_password_check_placeholder }}" name="new_password_check">
<input type="password" placeholder="{{ new_password_check_placeholder }}" name="new_password_check" required>
</label>
<br>
<button type="submit">{{ change_password_button }}</button>
Expand Down
1 change: 1 addition & 0 deletions templates/web/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<p>{{ welcome }}, {{ username }}!</p>
<p>{{ available_actions }}:</p>
<ol>
<li><a href="/admin/newsletters">{{ send_newsletter }}</li>
<li><a href="/admin/password">{{ change_password }}</li>
<li>
<form name="logoutForm" action="/admin/logout" method="post">
Expand Down
32 changes: 32 additions & 0 deletions templates/web/newsletter_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{% extends "base.html" %}

{% block content %}
{%- for flash in flashes %}
<p><i>{{ flash }}</i></p>
{%- endfor %}

<form action="/admin/newsletters" method="post">
<label>
{{ newsletter_title_label }}<br>
<input type="text" placeholder="{{ newsletter_title_placeholder }}" name="newsletter_title" required>
</label>
<br>
<br>
<label>
{{ newsletter_html_label }}<br>
<textarea placeholder="{{ newsletter_html_placeholder }}" rows="20" cols="100" name="newsletter_html"
required></textarea>
</label>
<br>
<br>
<label>
{{ newsletter_text_label }}<br>
<textarea type="text" placeholder="{{ newsletter_text_placeholder }}" rows="20" cols="100"
name="newsletter_text" required></textarea>
</label>
<br>
<br>
<button type="submit">{{ send_newsletter_button }}</button>
</form>
<p><a href="/admin/dashboard">&lt;- {{ back_link }}</a></p>
{% endblock %}
76 changes: 41 additions & 35 deletions tests/api/admin_newsletters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<p>Newsletter body as html.</p>",
}
});
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"))
Expand All @@ -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);
Expand All @@ -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": "<p>Newsletter body as html.</p>",
}
});
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())
Expand All @@ -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);
Expand All @@ -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": "<p>Newsletter body as html.</p>",
}
}),
"\
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",
),
];

Expand All @@ -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!(
Expand All @@ -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": "<p>Newsletter body as html.</p>",
}
});
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");
Expand Down
9 changes: 5 additions & 4 deletions tests/api/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 0ccedba

Please sign in to comment.