use super::Database; use crate::db::{errors::DataError, Users}; use crate::error::BotError; use crate::models::{AccountStatus, User}; use async_trait::async_trait; use std::convert::From; #[derive(Eq, PartialEq, Debug, Default, sqlx::FromRow)] struct UserRow { pub username: String, pub password: Option, pub active_room: Option, pub account_status: Option, } impl From for User { fn from(row: UserRow) -> Self { User { username: row.username, password: row.password, active_room: row.active_room, account_status: row.account_status.unwrap_or_default(), } } } #[async_trait] impl Users for Database { async fn upsert_user(&self, user: &User) -> Result<(), DataError> { sqlx::query( r#"INSERT INTO accounts (user_id, password) VALUES (?, ?) ON CONFLICT(user_id) DO UPDATE SET password = ?"#, ) .bind(&user.username) .bind(&user.password) .bind(&user.password) .execute(&self.conn) .await?; Ok(()) } async fn delete_user(&self, username: &str) -> Result<(), DataError> { sqlx::query(r#"DELETE FROM accounts WHERE user_id = ?"#) .bind(&username) .execute(&self.conn) .await?; Ok(()) } async fn get_user(&self, username: &str) -> Result, DataError> { // Should be query_as! macro, but the left join breaks it with a // non existing error message. let user_row: Option = sqlx::query_as( r#"SELECT a.user_id as "username", a.password, s.active_room, s.account_status FROM accounts a LEFT JOIN user_state s on a.user_id = s.user_id WHERE a.user_id = ?"#, ) .bind(username) .fetch_optional(&self.conn) .await?; Ok(user_row.map(|r| r.into())) } async fn authenticate_user( &self, username: &str, raw_password: &str, ) -> Result, BotError> { let user = self.get_user(username).await?; println!( "user pw is {:?}", user.as_ref().map(|u| u.password.as_ref()) ); Ok(user.filter(|u| u.verify_password(raw_password))) } } #[cfg(test)] mod tests { use super::*; use crate::db::sqlite::Database; use crate::db::Users; //TODO test selecting user when state doesn't exist. async fn create_db() -> Database { let db_path = tempfile::NamedTempFile::new_in(".").unwrap(); crate::db::sqlite::migrator::migrate(db_path.path().to_str().unwrap()) .await .unwrap(); Database::new(db_path.path().to_str().unwrap()) .await .unwrap() } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn create_and_get_user_test() { let db = create_db().await; let insert_result = db .upsert_user(&User { username: "myuser".to_string(), password: "abc".to_string(), }) .await; assert!(insert_result.is_ok()); let user = db .get_user("myuser") .await .expect("User retrieval query failed"); assert!(user.is_some()); let user = user.unwrap(); assert_eq!(user.username, "myuser"); assert_eq!(user.password, "abc"); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn can_update_user() { let db = create_db().await; let insert_result1 = db .upsert_user(&User { username: "myuser".to_string(), password: "abc".to_string(), }) .await; assert!(insert_result1.is_ok()); let insert_result2 = db .upsert_user(&User { username: "myuser".to_string(), password: "123".to_string(), }) .await; assert!(insert_result2.is_ok()); let user = db .get_user("myuser") .await .expect("User retrieval query failed"); assert!(user.is_some()); let user = user.unwrap(); assert_eq!(user.username, "myuser"); assert_eq!(user.password, "123"); //From second upsert } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn can_delete_user() { let db = create_db().await; let insert_result = db .upsert_user(&User { username: "myuser".to_string(), password: "abc".to_string(), }) .await; assert!(insert_result.is_ok()); db.delete_user("myuser") .await .expect("User deletion query failed"); let user = db .get_user("myuser") .await .expect("User retrieval query failed"); assert!(user.is_none()); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn username_not_in_db_returns_none() { let db = create_db().await; let user = db .get_user("does not exist") .await .expect("Get user query failure"); assert!(user.is_none()); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn authenticate_user_is_some_with_valid_password() { let db = create_db().await; let insert_result = db .upsert_user(&User { username: "myuser".to_string(), password: crate::logic::hash_password("abc").expect("password hash error!"), }) .await; assert!(insert_result.is_ok()); let user = db .authenticate_user("myuser", "abc") .await .expect("User retrieval query failed"); assert!(user.is_some()); let user = user.unwrap(); assert_eq!(user.username, "myuser"); } #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn authenticate_user_is_none_with_wrong_password() { let db = create_db().await; let insert_result = db .upsert_user(&User { username: "myuser".to_string(), password: crate::logic::hash_password("abc").expect("password hash error!"), }) .await; assert!(insert_result.is_ok()); let user = db .authenticate_user("myuser", "wrong-password") .await .expect("User retrieval query failed"); assert!(user.is_none()); } }