Comment posting is idempotent + tests
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
use crate::{
|
||||
authentication::AuthenticatedUser,
|
||||
idempotency::{IdempotencyKey, save_response, try_processing},
|
||||
routes::{AdminError, AppError},
|
||||
startup::AppState,
|
||||
@@ -8,7 +7,7 @@ use crate::{
|
||||
use anyhow::Context;
|
||||
use askama::Template;
|
||||
use axum::{
|
||||
Extension, Form,
|
||||
Form,
|
||||
extract::State,
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
@@ -95,7 +94,6 @@ pub async fn publish_newsletter(
|
||||
base_url,
|
||||
..
|
||||
}): State<AppState>,
|
||||
Extension(AuthenticatedUser { user_id, .. }): Extension<AuthenticatedUser>,
|
||||
Form(form): Form<BodyData>,
|
||||
) -> Result<Response, AppError> {
|
||||
validate_form(&form).map_err(|e| AdminError::Publish(anyhow::anyhow!(e)))?;
|
||||
@@ -105,7 +103,7 @@ pub async fn publish_newsletter(
|
||||
.try_into()
|
||||
.map_err(AdminError::Idempotency)?;
|
||||
|
||||
let mut transaction = match try_processing(&connection_pool, &idempotency_key, user_id).await? {
|
||||
let mut transaction = match try_processing(&connection_pool, &idempotency_key).await? {
|
||||
crate::idempotency::NextAction::StartProcessing(t) => t,
|
||||
crate::idempotency::NextAction::ReturnSavedResponse(response) => {
|
||||
return Ok(response);
|
||||
@@ -129,7 +127,7 @@ pub async fn publish_newsletter(
|
||||
let message = String::from("Your email has been queued for delivery.");
|
||||
let template = MessageTemplate::success(message);
|
||||
let response = Html(template.render().unwrap()).into_response();
|
||||
let response = save_response(transaction, &idempotency_key, user_id, response)
|
||||
let response = save_response(transaction, &idempotency_key, response)
|
||||
.await
|
||||
.map_err(AdminError::UnexpectedError)?;
|
||||
Ok(response)
|
||||
|
||||
@@ -54,7 +54,7 @@ pub async fn create_post(
|
||||
.try_into()
|
||||
.map_err(AdminError::Idempotency)?;
|
||||
|
||||
let mut transaction = match try_processing(&connection_pool, &idempotency_key, user_id).await? {
|
||||
let mut transaction = match try_processing(&connection_pool, &idempotency_key).await? {
|
||||
crate::idempotency::NextAction::StartProcessing(t) => t,
|
||||
crate::idempotency::NextAction::ReturnSavedResponse(response) => {
|
||||
return Ok(response);
|
||||
@@ -75,7 +75,7 @@ pub async fn create_post(
|
||||
|
||||
let template = MessageTemplate::success("Your new post has been published!".into());
|
||||
let response = Html(template.render().unwrap()).into_response();
|
||||
let response = save_response(transaction, &idempotency_key, user_id, response)
|
||||
let response = save_response(transaction, &idempotency_key, response)
|
||||
.await
|
||||
.map_err(AdminError::UnexpectedError)?;
|
||||
Ok(response)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::routes::get_max_page;
|
||||
use crate::idempotency::{IdempotencyKey, save_response, try_processing};
|
||||
use crate::routes::{AdminError, get_max_page};
|
||||
use crate::templates::CommentsPageDashboardTemplate;
|
||||
use crate::{
|
||||
domain::CommentEntry,
|
||||
@@ -13,7 +14,7 @@ use axum::{
|
||||
extract::{Path, Query, State},
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use sqlx::PgPool;
|
||||
use sqlx::{Executor, PgPool, Postgres, Transaction};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
@@ -25,6 +26,7 @@ pub struct CommentPathParam {
|
||||
pub struct CommentForm {
|
||||
pub author: Option<String>,
|
||||
pub content: String,
|
||||
pub idempotency_key: String,
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Posting new comment", skip_all, fields(post_id = %post_id))]
|
||||
@@ -36,14 +38,29 @@ pub async fn post_comment(
|
||||
Form(form): Form<CommentForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
validate_form(&form)?;
|
||||
let comment_id = insert_comment(&connection_pool, post_id, form)
|
||||
|
||||
let idempotency_key: IdempotencyKey = form
|
||||
.idempotency_key
|
||||
.try_into()
|
||||
.map_err(AdminError::Idempotency)?;
|
||||
|
||||
let mut transaction = match try_processing(&connection_pool, &idempotency_key).await? {
|
||||
crate::idempotency::NextAction::StartProcessing(t) => t,
|
||||
crate::idempotency::NextAction::ReturnSavedResponse(response) => {
|
||||
return Ok(response);
|
||||
}
|
||||
};
|
||||
|
||||
insert_comment(&mut transaction, post_id, form.author, form.content)
|
||||
.await
|
||||
.context("Could not insert comment into database.")?;
|
||||
tracing::info!("new comment with id {} has been inserted", comment_id);
|
||||
|
||||
let template = HtmlTemplate(MessageTemplate::success(
|
||||
"Your comment has been posted.".into(),
|
||||
));
|
||||
Ok(template.into_response())
|
||||
let response = template.into_response();
|
||||
let response = save_response(transaction, &idempotency_key, response).await?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn validate_form(form: &CommentForm) -> Result<(), anyhow::Error> {
|
||||
@@ -55,17 +72,19 @@ fn validate_form(form: &CommentForm) -> Result<(), anyhow::Error> {
|
||||
|
||||
#[tracing::instrument(name = "Inserting new comment in database", skip_all, fields(comment_id = tracing::field::Empty))]
|
||||
async fn insert_comment(
|
||||
connection_pool: &PgPool,
|
||||
transaction: &mut Transaction<'static, Postgres>,
|
||||
post_id: Uuid,
|
||||
form: CommentForm,
|
||||
author: Option<String>,
|
||||
content: String,
|
||||
) -> Result<Uuid, sqlx::Error> {
|
||||
let author = form
|
||||
.author
|
||||
let author = author
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.map(|s| s.trim().to_string());
|
||||
let content = content.trim();
|
||||
|
||||
let comment_id = Uuid::new_v4();
|
||||
tracing::Span::current().record("comment_id", comment_id.to_string());
|
||||
sqlx::query!(
|
||||
let query = sqlx::query!(
|
||||
"
|
||||
INSERT INTO comments (comment_id, post_id, author, content)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
@@ -73,10 +92,9 @@ async fn insert_comment(
|
||||
comment_id,
|
||||
post_id,
|
||||
author,
|
||||
form.content.trim()
|
||||
)
|
||||
.execute(connection_pool)
|
||||
.await?;
|
||||
content,
|
||||
);
|
||||
transaction.execute(query).await?;
|
||||
Ok(comment_id)
|
||||
}
|
||||
|
||||
|
||||
@@ -151,9 +151,11 @@ pub async fn see_post(
|
||||
let comments = get_comments_page_for_post(&connection_pool, post_id, 1)
|
||||
.await
|
||||
.context("Failed to fetch latest comments")?;
|
||||
let idempotency_key = Uuid::new_v4().to_string();
|
||||
let template = HtmlTemplate(PostTemplate {
|
||||
post,
|
||||
comments,
|
||||
idempotency_key,
|
||||
current_page,
|
||||
max_page,
|
||||
comments_count,
|
||||
|
||||
Reference in New Issue
Block a user