Add missing service and tests for the new handlers
This commit is contained in:
parent
c77e58f21b
commit
e525427f3e
5 changed files with 156 additions and 20 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -1969,6 +1969,7 @@ dependencies = [
|
|||
"axum-extra",
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tower",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ axum = { version = "0.8", features = ["macros"] }
|
|||
axum-extra = { version = "0.12.5", features = ["cookie"] }
|
||||
chrono = "0.4.44"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sqlx = { version = "0.8.6", features = [
|
||||
"runtime-tokio",
|
||||
"postgres",
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ use crate::{
|
|||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/posts/{slug}/vote", post(upvote_handler))
|
||||
.route("/posts/{slug}/vote", delete(downvote_handler))
|
||||
.route("/posts/{slug}/vote", delete(delete_vote_handler))
|
||||
.route("/posts/{slug}/votes", get(get_votes_and_voted_handler))
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ async fn upvote_handler(
|
|||
}
|
||||
}
|
||||
|
||||
async fn downvote_handler(
|
||||
async fn delete_vote_handler(
|
||||
jar: CookieJar,
|
||||
Path(slug): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -68,10 +68,14 @@ async fn downvote_handler(
|
|||
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"),
|
||||
Ok(()) => (StatusCode::OK, jar, "Successfully deleted vote"),
|
||||
Err(err) => {
|
||||
println!("{err}");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, jar, "Failed to upvote")
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
jar,
|
||||
"Failed to delete vote",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -97,19 +101,17 @@ mod tests {
|
|||
body::{Body, to_bytes},
|
||||
http::{Request, StatusCode, header},
|
||||
};
|
||||
use sqlx::{PgPool, query, query_scalar};
|
||||
use serde_json::Value;
|
||||
use sqlx::{PgPool, query_scalar};
|
||||
use tower::ServiceExt;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{env::Env, state::AppState, test_helpers::db::test_pool};
|
||||
|
||||
async fn delete_votes_for_slug(slug: &str, db: &PgPool) {
|
||||
query("delete from votes where slug = $1")
|
||||
.bind(slug)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
use crate::{
|
||||
env::Env,
|
||||
state::AppState,
|
||||
test_helpers::db::test_pool,
|
||||
votes::repository::{delete_votes_for_slug, insert_new_vote},
|
||||
};
|
||||
|
||||
async fn get_vote_count_for_slug(slug: &str, db: &PgPool) -> i64 {
|
||||
query_scalar("select count(*) from votes where slug = $1")
|
||||
|
|
@ -132,7 +134,7 @@ mod tests {
|
|||
async fn upvote_sets_cookie_and_inserts_vote() {
|
||||
let db = test_pool().await;
|
||||
let slug = "upvote_handler_sets_cookie_and_inserts_vote";
|
||||
delete_votes_for_slug(slug, &db).await;
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
|
||||
let app = crate::app(test_state(db.clone()));
|
||||
|
||||
|
|
@ -152,7 +154,7 @@ mod tests {
|
|||
assert_eq!(&body[..], b"Successfully upvoted");
|
||||
assert_eq!(get_vote_count_for_slug(slug, &db).await, 1);
|
||||
|
||||
delete_votes_for_slug(slug, &db).await;
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
@ -160,7 +162,7 @@ mod tests {
|
|||
let db = test_pool().await;
|
||||
let slug = "upvote_handler_idempotent_with_same_cookie";
|
||||
let voter_id = Uuid::from_u128(0xabc);
|
||||
delete_votes_for_slug(slug, &db).await;
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
|
||||
let cookie = format!("voter_id={voter_id}");
|
||||
|
||||
|
|
@ -179,14 +181,14 @@ mod tests {
|
|||
}
|
||||
|
||||
assert_eq!(get_vote_count_for_slug(slug, &db).await, 1);
|
||||
delete_votes_for_slug(slug, &db).await;
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn upvote_replaces_malformed_cookie() {
|
||||
let db = test_pool().await;
|
||||
let slug = "upvote_handler_replaces_malformed_cookie";
|
||||
delete_votes_for_slug(slug, &db).await;
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
|
||||
let app = crate::app(test_state(db.clone()));
|
||||
let request = Request::builder()
|
||||
|
|
@ -206,6 +208,112 @@ mod tests {
|
|||
assert!(!set_cookie.contains("not-a-uuid"));
|
||||
assert_eq!(get_vote_count_for_slug(slug, &db).await, 1);
|
||||
|
||||
delete_votes_for_slug(slug, &db).await;
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_vote_removes_vote_for_cookie_voter() {
|
||||
let db = test_pool().await;
|
||||
let slug = "delete_vote_handler_removes_vote_for_cookie_voter";
|
||||
let voter_id = Uuid::from_u128(0xdef);
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
insert_new_vote(slug, &voter_id, &db).await.unwrap();
|
||||
|
||||
let app = crate::app(test_state(db.clone()));
|
||||
let request = Request::builder()
|
||||
.method("DELETE")
|
||||
.uri(format!("/posts/{slug}/vote"))
|
||||
.header(header::COOKIE, format!("voter_id={voter_id}"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = app.oneshot(request).await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||
assert_eq!(&body[..], b"Successfully deleted vote");
|
||||
assert_eq!(get_vote_count_for_slug(slug, &db).await, 0);
|
||||
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_vote_initializes_cookie_when_missing() {
|
||||
let db = test_pool().await;
|
||||
let slug = "delete_vote_handler_initializes_cookie_when_missing";
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
|
||||
let app = crate::app(test_state(db.clone()));
|
||||
let request = Request::builder()
|
||||
.method("DELETE")
|
||||
.uri(format!("/posts/{slug}/vote"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = app.oneshot(request).await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert!(response.headers().contains_key(header::SET_COOKIE));
|
||||
assert_eq!(get_vote_count_for_slug(slug, &db).await, 0);
|
||||
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_votes_and_voted_returns_count_and_voted_true_for_existing_vote() {
|
||||
let db = test_pool().await;
|
||||
let slug = "get_votes_and_voted_handler_returns_existing_vote";
|
||||
let voter_id = Uuid::from_u128(0x123);
|
||||
let other_voter_id = Uuid::from_u128(0x456);
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
insert_new_vote(slug, &voter_id, &db).await.unwrap();
|
||||
insert_new_vote(slug, &other_voter_id, &db).await.unwrap();
|
||||
|
||||
let app = crate::app(test_state(db.clone()));
|
||||
let request = Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("/posts/{slug}/votes"))
|
||||
.header(header::COOKIE, format!("voter_id={voter_id}"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = app.oneshot(request).await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||
let json: Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(json["vote_count"], 2);
|
||||
assert_eq!(json["voted"], true);
|
||||
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_votes_and_voted_sets_cookie_and_returns_voted_false_for_new_voter() {
|
||||
let db = test_pool().await;
|
||||
let slug = "get_votes_and_voted_handler_sets_cookie_for_new_voter";
|
||||
let existing_voter_id = Uuid::from_u128(0x789);
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
insert_new_vote(slug, &existing_voter_id, &db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let app = crate::app(test_state(db.clone()));
|
||||
let request = Request::builder()
|
||||
.method("GET")
|
||||
.uri(format!("/posts/{slug}/votes"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let response = app.oneshot(request).await.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert!(response.headers().contains_key(header::SET_COOKIE));
|
||||
let body = to_bytes(response.into_body(), usize::MAX).await.unwrap();
|
||||
let json: Value = serde_json::from_slice(&body).unwrap();
|
||||
assert_eq!(json["vote_count"], 1);
|
||||
assert_eq!(json["voted"], false);
|
||||
|
||||
delete_votes_for_slug(slug, &db).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@ use uuid::Uuid;
|
|||
|
||||
use crate::votes::model::BestSlugs;
|
||||
|
||||
pub async fn delete_votes_for_slug(slug: &str, db: &PgPool) -> Result<()> {
|
||||
query("delete from votes where slug = $1")
|
||||
.bind(slug)
|
||||
.execute(db)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
pub async fn vote_exists(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<bool> {
|
||||
let count: i64 = query_scalar("select count(*) from votes where slug=$1 and voter_id=$2")
|
||||
.bind(slug)
|
||||
|
|
|
|||
19
src/votes/service.rs
Normal file
19
src/votes/service.rs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
use anyhow::Result;
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::votes::{
|
||||
dto::VotesAndDidVote,
|
||||
repository::{get_vote_count_for_slug, vote_exists},
|
||||
};
|
||||
|
||||
pub async fn get_votes_and_voted(
|
||||
slug: &str,
|
||||
voter_id: &Uuid,
|
||||
db: &PgPool,
|
||||
) -> Result<VotesAndDidVote> {
|
||||
let vote_count = get_vote_count_for_slug(slug, db).await?;
|
||||
let voted = vote_exists(slug, voter_id, db).await?;
|
||||
|
||||
Ok(VotesAndDidVote { vote_count, voted })
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue