From c77e58f21b93259627c21f8a86eb3b010c9af164 Mon Sep 17 00:00:00 2001 From: Alex Selimov Date: Thu, 19 Mar 2026 14:43:39 -0400 Subject: [PATCH] Add vote_exists and additional delete/get vote count handler --- src/votes/dto.rs | 10 ++++----- src/votes/handlers.rs | 48 +++++++++++++++++++++++++++++++++++++---- src/votes/mod.rs | 1 + src/votes/repository.rs | 41 ++++++++++++++++++++++++++++++++++- 4 files changed, 90 insertions(+), 10 deletions(-) diff --git a/src/votes/dto.rs b/src/votes/dto.rs index 6cda382..174acf8 100644 --- a/src/votes/dto.rs +++ b/src/votes/dto.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; -#[derive(Deserialize)] -pub struct CreateVoteRequest {} - -#[derive(Serialize)] -pub struct VoteResponse {} +#[derive(Serialize, Deserialize)] +pub struct VotesAndDidVote { + pub vote_count: i64, + pub voted: bool, +} diff --git a/src/votes/handlers.rs b/src/votes/handlers.rs index d6dcf93..0fb7b9e 100644 --- a/src/votes/handlers.rs +++ b/src/votes/handlers.rs @@ -1,17 +1,26 @@ use axum::{ - Router, + Json, Router, extract::{Path, State}, http::StatusCode, response::IntoResponse, - routing::post, + routing::{delete, get, post}, }; use axum_extra::extract::{CookieJar, cookie::Cookie}; use uuid::Uuid; -use crate::{state::AppState, votes::repository::insert_new_vote}; +use crate::{ + state::AppState, + votes::{ + repository::{delete_vote, insert_new_vote}, + service::get_votes_and_voted, + }, +}; pub fn router() -> Router { - Router::new().route("/posts/{slug}/vote", post(upvote_handler)) + Router::new() + .route("/posts/{slug}/vote", post(upvote_handler)) + .route("/posts/{slug}/vote", delete(downvote_handler)) + .route("/posts/{slug}/votes", get(get_votes_and_voted_handler)) } fn get_or_init_voter_id(jar: CookieJar) -> (CookieJar, Uuid) { @@ -51,6 +60,37 @@ async fn upvote_handler( } } +async fn downvote_handler( + jar: CookieJar, + Path(slug): Path, + State(state): State, +) -> impl IntoResponse { + let (jar, voter_id) = get_or_init_voter_id(jar); + + match delete_vote(&slug, &voter_id, &state.db).await { + Ok(()) => (StatusCode::OK, jar, "Successfully upvoted"), + Err(err) => { + println!("{err}"); + (StatusCode::INTERNAL_SERVER_ERROR, jar, "Failed to upvote") + } + } +} + +async fn get_votes_and_voted_handler( + jar: CookieJar, + Path(slug): Path, + State(state): State, +) -> impl IntoResponse { + let (jar, voter_id) = get_or_init_voter_id(jar); + match get_votes_and_voted(&slug, &voter_id, &state.db).await { + Ok(votes_and_voted) => (StatusCode::OK, jar, Json(votes_and_voted)).into_response(), + Err(err) => { + println!("{err}"); + (StatusCode::INTERNAL_SERVER_ERROR, jar, "Failed to upvote").into_response() + } + } +} + #[cfg(test)] mod tests { use axum::{ diff --git a/src/votes/mod.rs b/src/votes/mod.rs index d53e2ac..76ef459 100644 --- a/src/votes/mod.rs +++ b/src/votes/mod.rs @@ -2,3 +2,4 @@ pub mod dto; pub mod handlers; pub mod model; pub mod repository; +pub mod service; diff --git a/src/votes/repository.rs b/src/votes/repository.rs index ee04afa..853095c 100644 --- a/src/votes/repository.rs +++ b/src/votes/repository.rs @@ -4,6 +4,14 @@ use uuid::Uuid; use crate::votes::model::BestSlugs; +pub async fn vote_exists(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result { + let count: i64 = query_scalar("select count(*) from votes where slug=$1 and voter_id=$2") + .bind(slug) + .bind(voter_id) + .fetch_one(db) + .await?; + Ok(count > 0) +} pub async fn insert_new_vote(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<()> { query( r#"insert into votes (slug, voter_id) @@ -61,7 +69,7 @@ mod postgres_tests { use crate::{ test_helpers::db::test_pool, votes::repository::{ - delete_vote, get_top_n_slugs, get_vote_count_for_slug, insert_new_vote, + delete_vote, get_top_n_slugs, get_vote_count_for_slug, insert_new_vote, vote_exists, }, }; @@ -230,4 +238,35 @@ mod postgres_tests { cleanup(&db, &votes).await; } + + #[tokio::test] + pub async fn vote_exists_test() { + let db = test_pool().await; + let votes = vec![ + ( + "vote_exists_test_blog_post1".to_string(), + Uuid::from_u128(0x1), + ), + ( + "vote_exists_test_blog_post2".to_string(), + Uuid::from_u128(0x2), + ), + ( + "vote_exists_test_blog_post3".to_string(), + Uuid::from_u128(0x3), + ), + ]; + cleanup(&db, &votes).await; + + insert_new_vote(&votes[0].0, &votes[0].1, &db) + .await + .unwrap(); + insert_new_vote(&votes[1].0, &votes[1].1, &db) + .await + .unwrap(); + + assert!(vote_exists(&votes[0].0, &votes[0].1, &db).await.unwrap()); + assert!(vote_exists(&votes[1].0, &votes[1].1, &db).await.unwrap()); + assert!(!vote_exists(&votes[2].0, &votes[2].1, &db).await.unwrap()); + } }