Implement idempotency for insertion

This commit is contained in:
Alex Selimov 2026-03-19 13:44:11 -04:00
parent e15c30fe06
commit ea0c26a151

View file

@ -5,7 +5,11 @@ use uuid::Uuid;
use crate::votes::model::BestSlugs; use crate::votes::model::BestSlugs;
pub async fn insert_new_vote(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<()> { pub async fn insert_new_vote(slug: &str, voter_id: &Uuid, db: &PgPool) -> Result<()> {
query("insert into votes (slug, voter_id) values ($1, $2)") query(
r#"insert into votes (slug, voter_id)
values ($1, $2)
on conflict (slug, voter_id) do nothing"#,
)
.bind(slug) .bind(slug)
.bind(voter_id) .bind(voter_id)
.execute(db) .execute(db)
@ -56,26 +60,14 @@ mod postgres_tests {
use crate::{ use crate::{
test_helpers::db::test_pool, test_helpers::db::test_pool,
votes::repository::{delete_vote, get_top_n_slugs, get_vote_count_for_slug, insert_new_vote}, votes::repository::{
delete_vote, get_top_n_slugs, get_vote_count_for_slug, insert_new_vote,
},
}; };
fn test_votes() -> [(&'static str, Uuid); 9] { async fn cleanup(db: &PgPool, votes: &[(String, Uuid)]) {
[ for (slug, voter_id) in votes {
("blog_post1", Uuid::from_u128(0x1)), delete_vote(slug, voter_id, db).await.unwrap()
("blog_post1", Uuid::from_u128(0x2)),
("blog_post2", Uuid::from_u128(0x3)),
("blog_post2", Uuid::from_u128(0x4)),
("blog_post3", Uuid::from_u128(0x5)),
("blog_post3", Uuid::from_u128(0x6)),
("blog_post1", Uuid::from_u128(0x7)),
("blog_post1", Uuid::from_u128(0x8)),
("blog_post3", Uuid::from_u128(0x9)),
]
}
async fn cleanup(db: &PgPool) {
for (slug, voter_id) in test_votes() {
delete_vote(slug, &voter_id, db).await.unwrap()
} }
} }
@ -83,8 +75,18 @@ mod postgres_tests {
#[ignore] #[ignore]
pub async fn postgres_tests() { pub async fn postgres_tests() {
let db = test_pool().await; let db = test_pool().await;
cleanup(&db).await; let votes = vec![
let votes = test_votes(); ("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() { for (slug, voter_id) in votes.iter() {
insert_new_vote(slug, voter_id, &db) insert_new_vote(slug, voter_id, &db)
@ -92,25 +94,79 @@ mod postgres_tests {
.expect("Insertions to db failed"); .expect("Insertions to db failed");
} }
assert_eq!(get_vote_count_for_slug("blog_post1", &db).await.unwrap(), 4); assert_eq!(
assert_eq!(get_vote_count_for_slug("blog_post2", &db).await.unwrap(), 2); get_vote_count_for_slug("postgres_tests_blog_post1", &db)
assert_eq!(get_vote_count_for_slug("blog_post3", &db).await.unwrap(), 3); .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(); let top_2 = get_top_n_slugs(2, &db).await.unwrap();
assert_eq!(top_2[0].slug, "blog_post1"); assert_eq!(top_2[0].slug, "postgres_tests_blog_post1");
assert_eq!(top_2[1].slug, "blog_post3"); assert_eq!(top_2[1].slug, "postgres_tests_blog_post3");
delete_vote(votes[4].0, &votes[4].1, &db).await.unwrap(); delete_vote(&votes[4].0, &votes[4].1, &db).await.unwrap();
delete_vote(votes[5].0, &votes[5].1, &db).await.unwrap(); delete_vote(&votes[5].0, &votes[5].1, &db).await.unwrap();
assert_eq!(get_vote_count_for_slug("blog_post1", &db).await.unwrap(), 4); assert_eq!(
assert_eq!(get_vote_count_for_slug("blog_post2", &db).await.unwrap(), 2); get_vote_count_for_slug("postgres_tests_blog_post1", &db)
assert_eq!(get_vote_count_for_slug("blog_post3", &db).await.unwrap(), 1); .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(); let top_2 = get_top_n_slugs(2, &db).await.unwrap();
assert_eq!(top_2[0].slug, "blog_post1"); assert_eq!(top_2[0].slug, "postgres_tests_blog_post1");
assert_eq!(top_2[1].slug, "blog_post2"); assert_eq!(top_2[1].slug, "postgres_tests_blog_post2");
cleanup(&db).await; cleanup(&db, &votes).await;
}
#[tokio::test]
#[ignore]
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;
} }
} }