diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d6b130c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.claude/settings.local.json diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 577c289..b255661 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -23,6 +23,27 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + [[package]] name = "atoi" version = "2.0.0" @@ -102,11 +123,15 @@ dependencies = [ name = "backend" version = "0.1.0" dependencies = [ + "anyhow", "axum", + "chrono", + "serde", "sqlx", "tokio", "tracing", "tracing-subscriber", + "uuid", ] [[package]] @@ -154,6 +179,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + [[package]] name = "byteorder" version = "1.5.0" @@ -166,12 +197,35 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -187,6 +241,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -435,7 +495,19 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -586,6 +658,30 @@ dependencies = [ "tower-service", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -720,6 +816,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -758,6 +864,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" dependencies = [ + "cc", "pkg-config", "vcpkg", ] @@ -828,7 +935,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -1029,6 +1136,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -1056,7 +1169,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", ] [[package]] @@ -1197,6 +1310,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signature" version = "2.2.0" @@ -1272,6 +1391,7 @@ checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "base64", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -1292,8 +1412,11 @@ dependencies = [ "sha2", "smallvec", "thiserror", + "tokio", + "tokio-stream", "tracing", "url", + "uuid", ] [[package]] @@ -1330,6 +1453,7 @@ dependencies = [ "sqlx-postgres", "sqlx-sqlite", "syn", + "tokio", "url", ] @@ -1344,6 +1468,7 @@ dependencies = [ "bitflags", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -1372,6 +1497,7 @@ dependencies = [ "stringprep", "thiserror", "tracing", + "uuid", "whoami", ] @@ -1385,6 +1511,7 @@ dependencies = [ "base64", "bitflags", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -1409,6 +1536,7 @@ dependencies = [ "stringprep", "thiserror", "tracing", + "uuid", "whoami", ] @@ -1419,6 +1547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -1434,6 +1563,7 @@ dependencies = [ "thiserror", "tracing", "url", + "uuid", ] [[package]] @@ -1548,6 +1678,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", + "bytes", "io-uring", "libc", "mio", @@ -1570,6 +1701,17 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.2" @@ -1706,6 +1848,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -1730,12 +1883,79 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasite" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "whoami" version = "1.6.1" @@ -1768,6 +1988,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -1907,6 +2186,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "writeable" version = "0.6.1" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index 94a4f74..78e617e 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -4,8 +4,13 @@ version = "0.1.0" edition = "2024" [dependencies] +anyhow = "1.0.99" axum = "0.8.4" -sqlx = "0.8.6" +chrono = "0.4.41" +serde = "1.0.219" +sqlx = { version = "0.8.6", features = ["sqlite", "runtime-tokio", "uuid", "chrono"] } tokio = { version = "1.47.1", features = ["rt-multi-thread", "tracing"] } tracing = "0.1.41" tracing-subscriber = "0.3.19" +uuid = { version = "1.18.0", features = ["v4"] } + diff --git a/backend/src/database/connection.rs b/backend/src/database/connection.rs new file mode 100644 index 0000000..f2183ff --- /dev/null +++ b/backend/src/database/connection.rs @@ -0,0 +1,45 @@ +use anyhow::Result; +use sqlx::{SqlitePool, sqlite::SqliteConnectOptions}; +use std::str::FromStr; + +/// Database configuration +pub struct DatabaseConfig { + pub database_url: String, +} + +impl Default for DatabaseConfig { + fn default() -> Self { + Self { + database_url: "sqlite:local.db".to_string(), + } + } +} + +/// Create a SQLx connection pool +pub async fn create_pool(config: &DatabaseConfig) -> Result { + let options = SqliteConnectOptions::from_str(&config.database_url)?.create_if_missing(true); + + let pool = SqlitePool::connect_with(options).await?; + + sqlx::migrate!("./migrations").run(&pool).await?; + + Ok(pool) +} + +/// Initialize database with connection pool +pub async fn initialize_database() -> Result { + let config = DatabaseConfig::default(); + create_pool(&config).await +} + +#[cfg(test)] +pub async fn create_test_pool() -> Result { + let options = SqliteConnectOptions::from_str("sqlite::memory:")?.create_if_missing(true); + + let pool = SqlitePool::connect_with(options).await?; + + sqlx::migrate!("./migrations").run(&pool).await?; + + Ok(pool) +} + diff --git a/backend/src/database/mod.rs b/backend/src/database/mod.rs new file mode 100644 index 0000000..3525a77 --- /dev/null +++ b/backend/src/database/mod.rs @@ -0,0 +1,3 @@ +pub mod connection; + +pub use connection::*; \ No newline at end of file diff --git a/backend/src/main.rs b/backend/src/main.rs index 1cfd60b..8148b77 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,5 +1,8 @@ use axum::{Router, routing::get}; +mod models; +mod database; + #[tokio::main] async fn main() { tracing_subscriber::fmt::init(); diff --git a/backend/src/models/mod.rs b/backend/src/models/mod.rs new file mode 100644 index 0000000..e05a0c1 --- /dev/null +++ b/backend/src/models/mod.rs @@ -0,0 +1,3 @@ +mod task; + +pub use task::*; diff --git a/backend/src/models/task.rs b/backend/src/models/task.rs new file mode 100644 index 0000000..a633a1b --- /dev/null +++ b/backend/src/models/task.rs @@ -0,0 +1,269 @@ +use anyhow::{Result, bail}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::SqlitePool; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, sqlx::Type)] +#[serde(rename_all = "snake_case")] +#[sqlx(rename_all = "lowercase")] +pub enum TaskStatus { + Todo, + Done, + Backlog, +} + +#[derive(sqlx::FromRow)] +pub struct TaskModel { + pub id: Uuid, + pub title: String, + pub description: Option, + pub status: TaskStatus, + pub created_at: DateTime, + pub updated_at: DateTime, + pub completed_at: Option>, +} + +impl TaskModel { + pub async fn insert( + pool: &SqlitePool, + title: &str, + description: Option<&str>, + ) -> Result { + if title.len() == 0 { + bail!("Title must not be empty"); + } + + let id = Uuid::new_v4(); + let result = sqlx::query_as( + "INSERT INTO tasks (id, title, description) VALUES ($1, $2, $3) RETURNING *", + ) + .bind(id) + .bind(title) + .bind(description) + .fetch_one(pool) + .await?; + Ok(result) + } + + pub async fn get_by_id(pool: &SqlitePool, id: Uuid) -> Result { + let result = sqlx::query_as("SELECT * FROM tasks WHERE id = $1") + .bind(id) + .fetch_one(pool) + .await?; + Ok(result) + } + + pub async fn update(self, pool: &SqlitePool) -> Result { + let now: DateTime = Utc::now(); + + let _ = sqlx::query( + "UPDATE tasks + SET title=$1, description=$2, status=$3, updated_at=$4, completed_at=$5 + WHERE id=$6", + ) + .bind(&self.title) + .bind(&self.description) + .bind(&self.status) + .bind(now) + .bind(&self.completed_at) + .bind(&self.id) + .execute(pool) + .await?; + + TaskModel::get_by_id(pool, self.id).await + } + + pub async fn delete(pool: &SqlitePool, id: Uuid) -> Result<()> { + sqlx::query("DELETE FROM tasks WHERE id = $1") + .bind(id) + .execute(pool) + .await?; + Ok(()) + } + + pub async fn list_all(pool: &SqlitePool) -> Result> { + let tasks = sqlx::query_as("SELECT * FROM tasks ORDER BY created_at DESC") + .fetch_all(pool) + .await?; + Ok(tasks) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::database::create_test_pool; + + async fn setup_test_db() -> SqlitePool { + create_test_pool() + .await + .expect("Failed to create test pool") + } + + #[tokio::test] + async fn test_insert_task() { + let pool = setup_test_db().await; + + let title = "Test Task"; + let description = "Test Description"; + + let task = TaskModel::insert(&pool, title, Some(description)) + .await + .expect("Failed to insert task"); + + assert_eq!(task.title, title); + assert_eq!(task.description, Some(description.to_string())); + assert_eq!(task.status, TaskStatus::Todo); + assert!(task.completed_at.is_none()); + } + + #[tokio::test] + async fn test_insert_task_with_null_description() { + let pool = setup_test_db().await; + + let title = "Test Task"; + let description = None; + + let task = TaskModel::insert(&pool, title, description) + .await + .expect("Failed to insert task"); + + assert_eq!(task.title, title); + assert_eq!(task.description, None); + assert_eq!(task.status, TaskStatus::Todo); + } + + #[tokio::test] + async fn test_get_by_id_found() { + let pool = setup_test_db().await; + + let title = "Test Task"; + let description = Some("Test Description"); + + let inserted_task = TaskModel::insert(&pool, title, description) + .await + .expect("Failed to insert task"); + + let retrieved_task = TaskModel::get_by_id(&pool, inserted_task.id) + .await + .expect("Failed to get task by id"); + + assert_eq!(retrieved_task.id, inserted_task.id); + assert_eq!(retrieved_task.title, inserted_task.title); + assert_eq!(retrieved_task.description, inserted_task.description); + } + + #[tokio::test] + async fn test_get_by_id_not_found() { + let pool = setup_test_db().await; + + let non_existent_id = Uuid::new_v4(); + let result = TaskModel::get_by_id(&pool, non_existent_id).await; + + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_update_task() { + let pool = setup_test_db().await; + + let title = "Original Task"; + let description = Some("Original Description"); + + let mut task = TaskModel::insert(&pool, title, description) + .await + .expect("Failed to insert task"); + + // Update the task + task.title = "Updated Task".to_string(); + task.description = Some("Updated Description".to_string()); + task.status = TaskStatus::Done; + task.completed_at = Some(Utc::now()); + + let updated_task = task.update(&pool).await.expect("Failed to update task"); + + assert_eq!(updated_task.title, "Updated Task"); + assert_eq!( + updated_task.description, + Some("Updated Description".to_string()) + ); + assert_eq!(updated_task.status, TaskStatus::Done); + assert!(updated_task.completed_at.is_some()); + assert!(updated_task.updated_at > updated_task.created_at); + } + + #[tokio::test] + async fn test_delete_task() { + let pool = setup_test_db().await; + + let title = "Task to Delete"; + let description = Some("This will be deleted"); + + let task = TaskModel::insert(&pool, title, description) + .await + .expect("Failed to insert task"); + + // Delete the task + TaskModel::delete(&pool, task.id) + .await + .expect("Failed to delete task"); + + // Verify it's gone + let result = TaskModel::get_by_id(&pool, task.id).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_list_all_tasks() { + let pool = setup_test_db().await; + + // Insert multiple tasks + let _task1 = TaskModel::insert(&pool, "Task 1", Some("Desc 1")) + .await + .expect("Failed to insert task 1"); + + let _task2 = TaskModel::insert(&pool, "Task 2", None) + .await + .expect("Failed to insert task 2"); + + let tasks = TaskModel::list_all(&pool) + .await + .expect("Failed to list tasks"); + + assert_eq!(tasks.len(), 2); + + // Check that both tasks are present (order may vary due to timing) + let titles: Vec<&String> = tasks.iter().map(|t| &t.title).collect(); + assert!(titles.contains(&&"Task 1".to_string())); + assert!(titles.contains(&&"Task 2".to_string())); + } + + #[tokio::test] + async fn test_task_status_serialization() { + let pool = setup_test_db().await; + + // Test each status + let statuses = vec![TaskStatus::Todo, TaskStatus::Done, TaskStatus::Backlog]; + + for status in statuses { + let mut task = TaskModel::insert(&pool, "Test Task", None) + .await + .expect("Failed to insert task"); + + task.status = status; + task = task.update(&pool).await.expect("Failed to update task"); + + assert_eq!(task.status, status); + } + } + + #[tokio::test] + async fn test_empty_title_constraint() { + let pool = setup_test_db().await; + + let result = TaskModel::insert(&pool, "", None).await; + + assert!(result.is_err()); + } +}