use anyhow::Result; use sqlx::{PgPool, query, query_as, query_scalar}; use uuid::Uuid; use crate::votes::model::BestSlugs; pub async fn insert_new_vote(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<()> { query( r#"insert into votes (slug, voter_id) values ($1, $2) on conflict (slug, voter_id) do nothing"#, ) .bind(slug) .bind(voter_id) .execute(db) .await?; Ok(()) } pub async fn get_vote_count_for_slug(slug: &str, db: &PgPool) -> Result { let count: i64 = query_scalar("select count(*) from votes where slug=$1") .bind(slug) .fetch_one(db) .await?; Ok(count) } pub async fn get_top_n_slugs(n: i64, db: &PgPool) -> Result> { if n > 0 { let top_slugs = query_as::<_, BestSlugs>( r#"select slug, count(*) AS vote_count from votes group by slug order by vote_count desc limit $1 "#, ) .bind(n) .fetch_all(db) .await?; Ok(top_slugs) } else { Ok(vec![]) } } pub async fn delete_vote(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<()> { query("delete from votes where slug=$1 and voter_id=$2") .bind(slug) .bind(voter_id) .execute(db) .await?; Ok(()) } #[cfg(test)] mod postgres_tests { use sqlx::PgPool; use uuid::Uuid; use crate::{ test_helpers::db::test_pool, votes::repository::{ delete_vote, get_top_n_slugs, get_vote_count_for_slug, insert_new_vote, }, }; async fn cleanup(db: &PgPool, votes: &[(String, Uuid)]) { for (slug, voter_id) in votes { delete_vote(slug, voter_id, db).await.unwrap() } } #[tokio::test] pub async fn postgres_tests() { let db = test_pool().await; let votes = vec![ ( "postgres_tests_blog_post1".to_string(), Uuid::from_u128(0x1), ), ( "postgres_tests_blog_post1".to_string(), Uuid::from_u128(0x2), ), ( "postgres_tests_blog_post2".to_string(), Uuid::from_u128(0x3), ), ( "postgres_tests_blog_post2".to_string(), Uuid::from_u128(0x4), ), ( "postgres_tests_blog_post3".to_string(), Uuid::from_u128(0x5), ), ( "postgres_tests_blog_post3".to_string(), Uuid::from_u128(0x6), ), ( "postgres_tests_blog_post1".to_string(), Uuid::from_u128(0x7), ), ( "postgres_tests_blog_post1".to_string(), Uuid::from_u128(0x8), ), ( "postgres_tests_blog_post3".to_string(), Uuid::from_u128(0x9), ), ]; cleanup(&db, &votes).await; for (slug, voter_id) in votes.iter() { insert_new_vote(slug, voter_id, &db) .await .expect("Insertions to db failed"); } assert_eq!( get_vote_count_for_slug("postgres_tests_blog_post1", &db) .await .unwrap(), 4 ); assert_eq!( get_vote_count_for_slug("postgres_tests_blog_post2", &db) .await .unwrap(), 2 ); assert_eq!( get_vote_count_for_slug("postgres_tests_blog_post3", &db) .await .unwrap(), 3 ); let top_2 = get_top_n_slugs(2, &db).await.unwrap(); assert_eq!(top_2[0].slug, "postgres_tests_blog_post1"); assert_eq!(top_2[1].slug, "postgres_tests_blog_post3"); delete_vote(&votes[4].0, &votes[4].1, &db).await.unwrap(); delete_vote(&votes[5].0, &votes[5].1, &db).await.unwrap(); assert_eq!( get_vote_count_for_slug("postgres_tests_blog_post1", &db) .await .unwrap(), 4 ); assert_eq!( get_vote_count_for_slug("postgres_tests_blog_post2", &db) .await .unwrap(), 2 ); assert_eq!( get_vote_count_for_slug("postgres_tests_blog_post3", &db) .await .unwrap(), 1 ); let top_2 = get_top_n_slugs(2, &db).await.unwrap(); assert_eq!(top_2[0].slug, "postgres_tests_blog_post1"); assert_eq!(top_2[1].slug, "postgres_tests_blog_post2"); cleanup(&db, &votes).await; } #[tokio::test] pub async fn insert_idempotency_test() { let db = test_pool().await; let votes = vec![( "insert_idempotency_test_blog_post1".to_string(), Uuid::from_u128(0x1), )]; cleanup(&db, &votes).await; insert_new_vote(&votes[0].0, &votes[0].1, &db) .await .unwrap(); insert_new_vote(&votes[0].0, &votes[0].1, &db) .await .unwrap(); insert_new_vote(&votes[0].0, &votes[0].1, &db) .await .unwrap(); insert_new_vote(&votes[0].0, &votes[0].1, &db) .await .unwrap(); insert_new_vote(&votes[0].0, &votes[0].1, &db) .await .unwrap(); insert_new_vote(&votes[0].0, &votes[0].1, &db) .await .unwrap(); let votes_count = get_vote_count_for_slug(&votes[0].0, &db).await.unwrap(); assert_eq!(votes_count, 1); cleanup(&db, &votes).await; } #[tokio::test] pub async fn delete_idempotency_test() { let db = test_pool().await; let votes = vec![( "delete_idempotency_test_blog_post1".to_string(), Uuid::from_u128(0x1), )]; cleanup(&db, &votes).await; insert_new_vote(&votes[0].0, &votes[0].1, &db) .await .unwrap(); delete_vote(&votes[0].0, &votes[0].1, &db).await.unwrap(); delete_vote(&votes[0].0, &votes[0].1, &db).await.unwrap(); delete_vote(&votes[0].0, &votes[0].1, &db).await.unwrap(); let votes_count = get_vote_count_for_slug(&votes[0].0, &db).await.unwrap(); assert_eq!(votes_count, 0); cleanup(&db, &votes).await; } }