use crate::{ schema::captcha_answer, source::captcha_answer::CaptchaAnswer, utils::{functions::lower, get_conn, naive_now, DbPool}, }; use diesel::{ delete, dsl::exists, insert_into, result::Error, select, ExpressionMethods, QueryDsl, }; use diesel_async::RunQueryDsl; impl CaptchaAnswer { pub async fn insert(pool: &DbPool, captcha: &CaptchaAnswer) -> Result { let conn = &mut get_conn(pool).await?; insert_into(captcha_answer::table) .values(captcha) .get_result::(conn) .await } pub async fn check_captcha(pool: &DbPool, to_check: CaptchaAnswer) -> Result { let conn = &mut get_conn(pool).await?; // delete any expired captchas delete(captcha_answer::table.filter(captcha_answer::expires.lt(&naive_now()))) .execute(conn) .await?; // fetch requested captcha let captcha_exists = select(exists( captcha_answer::dsl::captcha_answer .filter((captcha_answer::dsl::uuid).eq(to_check.uuid.clone())) .filter(lower(captcha_answer::dsl::answer).eq(to_check.answer.to_lowercase().clone())), )) .get_result::(conn) .await?; // delete checked captcha delete(captcha_answer::table.filter(captcha_answer::uuid.eq(to_check.uuid.clone()))) .execute(conn) .await?; Ok(captcha_exists) } } #[cfg(test)] mod tests { use crate::{ source::captcha_answer::CaptchaAnswer, utils::{build_db_pool_for_tests, naive_now}, }; use chrono::Duration; use serial_test::serial; #[tokio::test] #[serial] async fn test_captcha_happy_path() { let pool = &build_db_pool_for_tests().await; let captcha_a_id = "a".to_string(); let _ = CaptchaAnswer::insert( pool, &CaptchaAnswer { uuid: captcha_a_id.clone(), answer: "XYZ".to_string(), expires: naive_now() + Duration::minutes(10), }, ) .await; let result = CaptchaAnswer::check_captcha( pool, CaptchaAnswer { uuid: captcha_a_id.clone(), answer: "xyz".to_string(), expires: chrono::NaiveDateTime::MIN, }, ) .await; assert!(result.is_ok()); assert!(result.unwrap()); } #[tokio::test] #[serial] async fn test_captcha_repeat_answer_fails() { let pool = &build_db_pool_for_tests().await; let captcha_a_id = "a".to_string(); let _ = CaptchaAnswer::insert( pool, &CaptchaAnswer { uuid: captcha_a_id.clone(), answer: "XYZ".to_string(), expires: naive_now() + Duration::minutes(10), }, ) .await; let result = CaptchaAnswer::check_captcha( pool, CaptchaAnswer { uuid: captcha_a_id.clone(), answer: "xyz".to_string(), expires: chrono::NaiveDateTime::MIN, }, ) .await; let result_repeat = CaptchaAnswer::check_captcha( pool, CaptchaAnswer { uuid: captcha_a_id.clone(), answer: "xyz".to_string(), expires: chrono::NaiveDateTime::MIN, }, ) .await; assert!(result_repeat.is_ok()); assert!(!result_repeat.unwrap()); } #[tokio::test] #[serial] async fn test_captcha_expired_fails() { let pool = &build_db_pool_for_tests().await; let expired_id = "already_expired".to_string(); let _ = CaptchaAnswer::insert( pool, &CaptchaAnswer { uuid: expired_id.clone(), answer: "xyz".to_string(), expires: naive_now() - Duration::seconds(1), }, ) .await; let expired_result = CaptchaAnswer::check_captcha( pool, CaptchaAnswer { uuid: expired_id.clone(), answer: "xyz".to_string(), expires: chrono::NaiveDateTime::MIN, }, ) .await; assert!(expired_result.is_ok()); assert!(!expired_result.unwrap()); } }