From 99321e9a5d6a21fb06c122ba22d315915b81aa45 Mon Sep 17 00:00:00 2001 From: Julia Lange Date: Thu, 5 Feb 2026 13:10:21 -0800 Subject: [PATCH] db, add core user functionality and tests; db mod Adds all the basic user functions, and tests for them. This is also intializes the DB Module, which will have more things in it --- koucha/src/db.rs | 11 +++ koucha/src/db/user.rs | 160 +++++++++++++++++++++++++++++++++++++++ koucha/src/lib.rs | 1 + koucha/src/test_utils.rs | 13 ++++ 4 files changed, 185 insertions(+) create mode 100644 koucha/src/db.rs create mode 100644 koucha/src/db/user.rs diff --git a/koucha/src/db.rs b/koucha/src/db.rs new file mode 100644 index 0000000..a19ee4a --- /dev/null +++ b/koucha/src/db.rs @@ -0,0 +1,11 @@ +mod user; +pub use user::User; + +macro_rules! define_key { + ($name:ident) => { + #[derive(PartialEq, Debug, Copy, Clone)] + pub struct $name(i64); + }; +} + +define_key!(UserKey); diff --git a/koucha/src/db/user.rs b/koucha/src/db/user.rs new file mode 100644 index 0000000..f42491b --- /dev/null +++ b/koucha/src/db/user.rs @@ -0,0 +1,160 @@ +use crate::{ + Result, + AdapterPool, + db::UserKey, +}; + +pub struct UnparsedUser { + pub id: i64, + pub name: String, +} +impl UnparsedUser { + pub fn parse(self) -> Result { + Ok(User { + key: UserKey(self.id), + name: self.name + }) + } +} + +pub struct User { + key: UserKey, + name: String, +} + +impl User { + pub fn key(&self) -> UserKey { self.key } + pub fn name(&self) -> &str { &self.name } + + pub async fn get(pool: &AdapterPool, key: UserKey) -> Result { + sqlx::query_as!( + UnparsedUser, + "SELECT id, name FROM users WHERE id = ?", + key.0 + ).fetch_one(&pool.0).await?.parse() + } + + pub async fn get_all(pool: &AdapterPool) -> Result> { + sqlx::query_as!( + UnparsedUser, + "SELECT id, name FROM users" + ).fetch_all(&pool.0).await?.into_iter().map(UnparsedUser::parse).collect() + } + + pub async fn create(pool: &AdapterPool, name: &str) -> Result { + sqlx::query_as!( + UnparsedUser, + "INSERT INTO users (name) + VALUES (?) + RETURNING id, name", + name + ).fetch_one(&pool.0).await?.parse() + } + + pub async fn update_name( + pool: &AdapterPool, key: UserKey, new_name: &str + ) -> Result<()> { + sqlx::query!( + "UPDATE users SET name = ? WHERE id = ?", + new_name, key.0 + ).execute(&pool.0).await?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + test_utils::{ + USERNAME, USERNAME2, + setup_adapter, + }, + }; + + #[test] + fn parse() { + const UID: i64 = 1; + let unparsed_user = UnparsedUser { + id: UID, + name: USERNAME.to_string(), + }; + + let user = unparsed_user.parse().unwrap(); + assert_eq!(user.key.0, UID); + assert_eq!(user.name, USERNAME); + } + + #[tokio::test] + async fn get() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + let new_user = User::create(pool, USERNAME).await.unwrap(); + + let fetched_user = User::get(pool, new_user.key).await.unwrap(); + assert_eq!(fetched_user.name, USERNAME); + assert_eq!(fetched_user.key.0, 1); + } + + #[tokio::test] + async fn get_all() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + User::create(pool, USERNAME).await.unwrap(); + User::create(pool, USERNAME2).await.unwrap(); + + let users = User::get_all(pool).await.unwrap(); + assert_eq!(users.len(), 2); + assert!(users.iter().any(|u| u.name == USERNAME)); + assert!(users.iter().any(|u| u.name == USERNAME2)); + } + + #[tokio::test] + async fn create_user() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + + let user = User::create(pool, USERNAME).await.unwrap(); + + assert_eq!(user.name, USERNAME); + assert_eq!(user.key.0, 1); + } + + #[tokio::test] + async fn create_duplicate_user() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + + User::create(pool, USERNAME).await.unwrap(); + let duplicate_user = User::create(pool, USERNAME).await; + + assert!(duplicate_user.is_err()); + } + + #[tokio::test] + async fn update_name() { + const NEW_USERNAME: &str = "Alicia"; + assert!(NEW_USERNAME != USERNAME); + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + + let user = User::create(pool, USERNAME).await.unwrap(); + User::update_name(pool, user.key, NEW_USERNAME).await.unwrap(); + + let updated = User::get(pool, user.key).await.unwrap(); + assert_eq!(updated.name, NEW_USERNAME); + } + + #[tokio::test] + async fn update_name_to_duplicate() { + let adapter = setup_adapter().await; + let pool = adapter.get_pool(); + + let user1 = User::create(pool, USERNAME).await.unwrap(); + User::create(pool, USERNAME2).await.unwrap(); + let status = User::update_name(pool, user1.key, USERNAME2).await; + + assert!(status.is_err()); + } +} diff --git a/koucha/src/lib.rs b/koucha/src/lib.rs index 3bf607b..5982237 100644 --- a/koucha/src/lib.rs +++ b/koucha/src/lib.rs @@ -2,6 +2,7 @@ use std::error::Error; type Result = std::result::Result>; +pub mod db; pub mod score; #[cfg(test)] diff --git a/koucha/src/test_utils.rs b/koucha/src/test_utils.rs index 6492538..7cd4601 100644 --- a/koucha/src/test_utils.rs +++ b/koucha/src/test_utils.rs @@ -1,11 +1,24 @@ #![cfg(test)] +use crate::{ + Adapter, + AdapterBuilder, +}; use chrono::{ Utc, TimeZone, DateTime }; +pub const USERNAME: &str = "Alice"; +pub const USERNAME2: &str = "Bob"; + pub fn get_datetime() -> DateTime { Utc.with_ymd_and_hms(2020,1,1,0,0,0).unwrap() } + +pub async fn setup_adapter() -> Adapter { + AdapterBuilder::new() + .database_url("sqlite::memory:") + .create().await.unwrap() +}