From ebb44fd7128c51f55b63f3c7d0b2c2decd17f051 Mon Sep 17 00:00:00 2001 From: Anthony Merlo Date: Thu, 20 Aug 2026 17:25:41 +0100 Subject: [PATCH] test: add 25-test suite + fix sum_nutriments display keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 25 integration tests: DB init, default goals, FTS5 cache, upsert, portion scaling (100g/50g/200g/empty/non-numeric), sum_nutriments (single/multiple/empty/partial/no-data), meal detection, date format, log entry + summary round trip, custom food + sum, delete entry, goals get/update, weight log + history + upsert - Fix: sum_nutriments now uses TRACKED_NUTRIENTS display names (first tuple element) instead of deriving from OFF field names — fixes proteins→protein, energy-kcal→energy_kcal mismatches - Fix: weight_log.date is now UNIQUE for proper upsert behavior - Fix: build_daily_summary remaining keys use display names - Added lib.rs for integration test access - Added regex dev-dependency Closes nutrition-mcp-45t --- Cargo.lock | 13 + Cargo.toml | 13 +- src/db.rs | 2 +- src/lib.rs | 7 + src/tools.rs | 15 +- tests/common/mod.rs | 37 +++ tests/integration_tests.rs | 515 +++++++++++++++++++++++++++++++++++++ 7 files changed, 592 insertions(+), 10 deletions(-) create mode 100644 src/lib.rs create mode 100644 tests/common/mod.rs create mode 100644 tests/integration_tests.rs diff --git a/Cargo.lock b/Cargo.lock index bf46aa5..e05047f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1100,6 +1100,7 @@ dependencies = [ "anyhow", "axum", "chrono", + "regex", "reqwest", "rmcp", "schemars", @@ -1436,6 +1437,18 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.18" diff --git a/Cargo.toml b/Cargo.toml index 1f3d1ca..5470eb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,14 @@ edition = "2024" description = "MCP server for personal calorie/nutrient tracking with OpenFood Facts integration" license = "MIT" +[lib] +name = "nutrition_mcp" +path = "src/lib.rs" + +[[bin]] +name = "nutrition-mcp" +path = "src/main.rs" + [dependencies] # MCP protocol rmcp = { version = "0.7", features = [ @@ -46,4 +54,7 @@ uuid = { version = "1", features = ["v4"] } [profile.release] opt-level = 2 -strip = true \ No newline at end of file +strip = true + +[dev-dependencies] +regex = "1" \ No newline at end of file diff --git a/src/db.rs b/src/db.rs index 1243a5c..2d42d3a 100644 --- a/src/db.rs +++ b/src/db.rs @@ -164,7 +164,7 @@ async fn run_migrations(pool: &DbPool, db_path: &Path) -> Result<()> { sqlx::query( r#"CREATE TABLE IF NOT EXISTS weight_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, - date TEXT NOT NULL, + date TEXT NOT NULL UNIQUE, weight_kg REAL NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')) )"#, diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3d10ed6 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,7 @@ +// Library crate — exposes modules for integration tests. +// The binary (main.rs) re-uses these same modules. + +pub mod api; +pub mod config; +pub mod db; +pub mod tools; \ No newline at end of file diff --git a/src/tools.rs b/src/tools.rs index 41cbc77..c1733f2 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -164,11 +164,11 @@ pub struct BulkImportParams { // ── Helper functions ──────────────────────────────────────────────── -fn today() -> String { +pub fn today() -> String { chrono::Local::now().format("%Y-%m-%d").to_string() } -fn detect_meal() -> String { +pub fn detect_meal() -> String { let hour = chrono::Local::now().format("%H").to_string().parse::().unwrap_or(12); match hour { h if h < 11 => "breakfast".to_string(), @@ -180,7 +180,7 @@ fn detect_meal() -> String { /// Scale nutrient JSON from per-100g to actual portion. /// multiplies each numeric value by (grams / 100). -fn scale_nutriments(nutriments: &Value, grams: f64) -> Value { +pub fn scale_nutriments(nutriments: &Value, grams: f64) -> Value { let factor = grams / 100.0; if let Some(obj) = nutriments.as_object() { let scaled: serde_json::Map = obj @@ -202,10 +202,10 @@ fn scale_nutriments(nutriments: &Value, grams: f64) -> Value { /// Build a summary JSON from a list of entries' nutriments. /// Sums all tracked nutrients, returns "no data" for missing ones. -fn sum_nutriments(entries_nutriments: &[Value]) -> Value { +pub fn sum_nutriments(entries_nutriments: &[Value]) -> Value { let mut totals: serde_json::Map = serde_json::Map::new(); - for (_, tracked_key) in TRACKED_NUTRIENTS { + for (display_key, tracked_key) in TRACKED_NUTRIENTS { let mut sum = 0.0; let mut has_data = false; for entry_nutriments in entries_nutriments { @@ -214,9 +214,8 @@ fn sum_nutriments(entries_nutriments: &[Value]) -> Value { has_data = true; } } - let display_key = tracked_key.replace("_100g", ""); totals.insert( - display_key, + display_key.to_string(), if has_data { json!(sum) } else { json!("no data") }, ); } @@ -780,7 +779,7 @@ impl NutritionServer { "totals": totals, "goals": goals, "remaining": { - "energy_kcal": remaining("energy-kcal"), + "energy_kcal": remaining("energy_kcal"), "protein": remaining("protein"), "fructose": remaining("fructose"), "salt": remaining("salt"), diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..4689788 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,37 @@ +// Common test utilities. + +use std::path::PathBuf; +use nutrition_mcp::db::{self, DbPool}; + +/// A temporary SQLite database for testing. +/// Creates a unique DB file in /tmp that is cleaned up on drop. +pub struct TestDb { + pub pool: DbPool, + _path: PathBuf, +} + +impl TestDb { + pub async fn new() -> Self { + let path = std::env::temp_dir().join(format!( + "nutrition-test-{}-{}.db", + std::process::id(), + uuid::Uuid::new_v4() + )); + let pool = db::init_database(&path).await.expect("failed to init test DB"); + Self { + pool, + _path: path, + } + } +} + +impl Drop for TestDb { + fn drop(&mut self) { + // Best-effort cleanup — ignore errors + let _ = std::fs::remove_file(&self._path); + let wal = self._path.with_extension("db-wal"); + let shm = self._path.with_extension("db-shm"); + let _ = std::fs::remove_file(&wal); + let _ = std::fs::remove_file(&shm); + } +} \ No newline at end of file diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs new file mode 100644 index 0000000..255e8e5 --- /dev/null +++ b/tests/integration_tests.rs @@ -0,0 +1,515 @@ +// Integration tests for nutrition-mcp. +// Uses temp SQLite DBs — no network calls. + +mod common; + +#[cfg(test)] +mod tests { + use super::common::TestDb; + use nutrition_mcp::api::{OffClient, Product}; + use nutrition_mcp::db; + use nutrition_mcp::tools; + use serde_json::{json, Value}; + + // ── Database tests ──────────────────────────────────────────── + + #[tokio::test] + async fn test_db_init_creates_all_tables() { + let _db = TestDb::new().await; + // If init succeeds without panic, all tables were created. + // The TestDb constructor calls db::init_database. + } + + #[tokio::test] + async fn test_db_seeds_default_goals() { + let db = TestDb::new().await; + let row = sqlx::query("SELECT goals FROM daily_goal WHERE id = 1") + .fetch_one(&db.pool) + .await + .unwrap(); + let goals_str: String = sqlx::Row::get(&row, "goals"); + let goals: Value = serde_json::from_str(&goals_str).unwrap(); + assert_eq!(goals["energy_kcal"], json!(2500)); + assert_eq!(goals["protein"], json!(150)); + assert_eq!(goals["fructose"], json!(15)); + assert_eq!(goals["alcohol"], json!(0)); + assert_eq!(goals["salt"], json!(6)); + assert_eq!(goals["potassium"], json!(3500)); + assert_eq!(goals["calcium"], json!(700)); + assert_eq!(goals["magnesium"], json!(300)); + assert_eq!(goals["cholesterol"], json!(300)); + } + + #[test] + fn test_tracked_nutrients_count() { + assert_eq!(db::TRACKED_NUTRIENTS.len(), 14); + } + + #[test] + fn test_tracked_nutrients_has_gout_and_hypertension_markers() { + let keys: Vec<&str> = db::TRACKED_NUTRIENTS.iter().map(|(k, _)| *k).collect(); + // Gout + assert!(keys.contains(&"fructose")); + assert!(keys.contains(&"alcohol")); + // Hypertension + assert!(keys.contains(&"salt")); + assert!(keys.contains(&"potassium")); + assert!(keys.contains(&"calcium")); + assert!(keys.contains(&"magnesium")); + assert!(keys.contains(&"cholesterol")); + // Core macros + assert!(keys.contains(&"energy_kcal")); + assert!(keys.contains(&"protein")); + assert!(keys.contains(&"carbohydrates")); + assert!(keys.contains(&"fat")); + assert!(keys.contains(&"fiber")); + assert!(keys.contains(&"sugars")); + assert!(keys.contains(&"saturated_fat")); + } + + // ── Cache tests (no network) ──────────────────────────────── + + #[tokio::test] + async fn test_cache_products_and_get() { + let db = TestDb::new().await; + let product = Product { + code: "123456789".to_string(), + product_name: "Test Blueberries".to_string(), + brands: Some("TestBrand".to_string()), + nutriments: json!({"energy-kcal_100g": 57.7, "proteins_100g": 0.87}).to_string(), + serving_quantity: Some(100.0), + serving_size: Some("100g".to_string()), + image_url: None, + }; + + OffClient::cache_products(&db.pool, &[product]).await.unwrap(); + + let cached = OffClient::cache_get(&db.pool, "123456789").await.unwrap(); + assert!(cached.is_some()); + let p = cached.unwrap(); + assert_eq!(p.product_name, "Test Blueberries"); + assert_eq!(p.brands, Some("TestBrand".to_string())); + } + + #[tokio::test] + async fn test_cache_search_fts5() { + let db = TestDb::new().await; + let products = vec![ + Product { + code: "111".to_string(), + product_name: "Organic Blueberries".to_string(), + brands: None, + nutriments: "{}".to_string(), + serving_quantity: None, + serving_size: None, + image_url: None, + }, + Product { + code: "222".to_string(), + product_name: "Chicken Breast".to_string(), + brands: None, + nutriments: "{}".to_string(), + serving_quantity: None, + serving_size: None, + image_url: None, + }, + ]; + OffClient::cache_products(&db.pool, &products).await.unwrap(); + + let results = OffClient::cache_search(&db.pool, "blueberries", 10).await.unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].product_name, "Organic Blueberries"); + + let results = OffClient::cache_search(&db.pool, "chicken", 10).await.unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].product_name, "Chicken Breast"); + } + + #[tokio::test] + async fn test_cache_products_upsert() { + let db = TestDb::new().await; + let p1 = Product { + code: "999".to_string(), + product_name: "Original Name".to_string(), + brands: None, + nutriments: "{}".to_string(), + serving_quantity: None, + serving_size: None, + image_url: None, + }; + OffClient::cache_products(&db.pool, &[p1]).await.unwrap(); + + let p2 = Product { + code: "999".to_string(), + product_name: "Updated Name".to_string(), + brands: Some("NewBrand".to_string()), + nutriments: "{}".to_string(), + serving_quantity: None, + serving_size: None, + image_url: None, + }; + OffClient::cache_products(&db.pool, &[p2]).await.unwrap(); + + let cached = OffClient::cache_get(&db.pool, "999").await.unwrap().unwrap(); + assert_eq!(cached.product_name, "Updated Name"); + assert_eq!(cached.brands, Some("NewBrand".to_string())); + } + + // ── Portion scaling tests ──────────────────────────────────── + + #[test] + fn test_scale_nutriments_100g() { + let n = json!({"energy-kcal_100g": 200.0, "proteins_100g": 10.0}); + let scaled = tools::scale_nutriments(&n, 100.0); + assert_eq!(scaled["energy-kcal_100g"], json!(200.0)); + assert_eq!(scaled["proteins_100g"], json!(10.0)); + } + + #[test] + fn test_scale_nutriments_50g() { + let n = json!({"energy-kcal_100g": 200.0, "proteins_100g": 10.0}); + let scaled = tools::scale_nutriments(&n, 50.0); + assert_eq!(scaled["energy-kcal_100g"], json!(100.0)); + assert_eq!(scaled["proteins_100g"], json!(5.0)); + } + + #[test] + fn test_scale_nutriments_200g() { + let n = json!({"energy-kcal_100g": 200.0, "proteins_100g": 10.0}); + let scaled = tools::scale_nutriments(&n, 200.0); + assert_eq!(scaled["energy-kcal_100g"], json!(400.0)); + assert_eq!(scaled["proteins_100g"], json!(20.0)); + } + + #[test] + fn test_scale_nutriments_empty() { + let n = json!({}); + let scaled = tools::scale_nutriments(&n, 50.0); + assert!(scaled.as_object().unwrap().is_empty()); + } + + #[test] + fn test_scale_nutriments_preserves_non_numeric() { + let n = json!({"energy-kcal_100g": 200.0, "unit": "kcal"}); + let scaled = tools::scale_nutriments(&n, 50.0); + assert_eq!(scaled["energy-kcal_100g"], json!(100.0)); + assert_eq!(scaled["unit"], json!("kcal")); // non-numeric preserved + } + + // ── Sum nutriments tests ───────────────────────────────────── + + #[test] + fn test_sum_nutriments_single_entry() { + let entries = vec![json!({"energy-kcal_100g": 250.0, "proteins_100g": 20.0})]; + let totals = tools::sum_nutriments(&entries); + assert_eq!(totals["energy_kcal"], json!(250.0)); + assert_eq!(totals["protein"], json!(20.0)); + } + + #[test] + fn test_sum_nutriments_multiple_entries() { + let entries = vec![ + json!({"energy-kcal_100g": 250.0, "proteins_100g": 20.0}), + json!({"energy-kcal_100g": 150.0, "proteins_100g": 10.0}), + ]; + let totals = tools::sum_nutriments(&entries); + assert_eq!(totals["energy_kcal"], json!(400.0)); + assert_eq!(totals["protein"], json!(30.0)); + } + + #[test] + fn test_sum_nutriments_no_data_for_missing() { + // Entry has only energy, not potassium — potassium should be "no data" + let entries = vec![json!({"energy-kcal_100g": 250.0})]; + let totals = tools::sum_nutriments(&entries); + assert_eq!(totals["energy_kcal"], json!(250.0)); + assert_eq!(totals["potassium"], json!("no data")); + } + + #[test] + fn test_sum_nutriments_empty() { + let entries: Vec = vec![]; + let totals = tools::sum_nutriments(&entries); + // All should be "no data" when no entries + assert_eq!(totals["energy_kcal"], json!("no data")); + assert_eq!(totals["protein"], json!("no data")); + assert_eq!(totals["fructose"], json!("no data")); + } + + #[test] + fn test_sum_nutriments_partial_data() { + // One entry has potassium, another doesn't — should sum available + let entries = vec![ + json!({"potassium_100g": 100.0}), + json!({"potassium_100g": 50.0, "energy-kcal_100g": 200.0}), + ]; + let totals = tools::sum_nutriments(&entries); + assert_eq!(totals["potassium"], json!(150.0)); + assert_eq!(totals["energy_kcal"], json!(200.0)); + } + + // ── Date/meal tests ────────────────────────────────────────── + + #[test] + fn test_today_format() { + let today = tools::today(); + assert!(regex::Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap().is_match(&today)); + } + + #[test] + fn test_detect_meal_returns_valid_value() { + let meal = tools::detect_meal(); + assert!( + meal == "breakfast" || meal == "lunch" || meal == "dinner" || meal == "snack", + "detect_meal returned invalid value: {meal}" + ); + } + + // ── Integration: entries round trips ───────────────────────── + + #[tokio::test] + async fn test_log_entry_and_daily_summary() { + let db = TestDb::new().await; + + // Insert a product into cache + let product = Product { + code: "blueberry-test".to_string(), + product_name: "Blueberries".to_string(), + brands: Some("TestBrand".to_string()), + nutriments: json!({ + "energy-kcal_100g": 57.7, + "proteins_100g": 0.87, + "carbohydrates_100g": 10.6, + "fat_100g": 0.33, + "fiber_100g": 2.4, + "sugars_100g": 9.96, + "fructose_100g": 4.97, + "salt_100g": 0.0025, + "saturated-fat_100g": 0.028, + "potassium_100g": 0.077, + "alcohol_100g": 0, + "cholesterol_100g": 0, + }) + .to_string(), + serving_quantity: Some(100.0), + serving_size: Some("100g".to_string()), + image_url: None, + }; + OffClient::cache_products(&db.pool, &[product]).await.unwrap(); + + // Log 50g of blueberries + let date = chrono::Local::now().format("%Y-%m-%d").to_string(); + let scaled = tools::scale_nutriments( + &json!({ + "energy-kcal_100g": 57.7, + "proteins_100g": 0.87, + "fructose_100g": 4.97, + "potassium_100g": 0.077, + }), + 50.0, + ); + + sqlx::query( + r#"INSERT INTO entries (date, meal, product_code, food_name, grams, nutriments) + VALUES (?, ?, ?, ?, ?, ?)"#, + ) + .bind(&date) + .bind("breakfast") + .bind("blueberry-test") + .bind("Blueberries") + .bind(50.0_f64) + .bind(scaled.to_string()) + .execute(&db.pool) + .await + .unwrap(); + + // Query entries for today + let rows = sqlx::query("SELECT food_name, grams, nutriments FROM entries WHERE date = ?") + .bind(&date) + .fetch_all(&db.pool) + .await + .unwrap(); + assert_eq!(rows.len(), 1); + + let n_str: String = sqlx::Row::get(&rows[0], "nutriments"); + let n: Value = serde_json::from_str(&n_str).unwrap(); + // 50g of 57.7 kcal/100g = 28.85 kcal + assert!((n["energy-kcal_100g"].as_f64().unwrap() - 28.85).abs() < 0.01); + // 50g of 4.97g fructose/100g = 2.485g + assert!((n["fructose_100g"].as_f64().unwrap() - 2.485).abs() < 0.001); + } + + #[tokio::test] + async fn test_log_custom_food_and_sum() { + let db = TestDb::new().await; + let date = "2026-01-15"; + + // Log a custom food: 200g of homemade salad, 150 kcal/100g + let nutriments = json!({ + "energy-kcal_100g": 150.0, + "proteins_100g": 5.0, + "carbohydrates_100g": 20.0, + "fat_100g": 6.0, + }); + let scaled = tools::scale_nutriments(&nutriments, 200.0); + + sqlx::query( + r#"INSERT INTO entries (date, meal, food_name, grams, nutriments) + VALUES (?, ?, ?, ?, ?)"#, + ) + .bind(date) + .bind("lunch") + .bind("Homemade Salad") + .bind(200.0_f64) + .bind(scaled.to_string()) + .execute(&db.pool) + .await + .unwrap(); + + // Fetch and sum + let rows = sqlx::query("SELECT nutriments FROM entries WHERE date = ?") + .bind(date) + .fetch_all(&db.pool) + .await + .unwrap(); + let entries_n: Vec = rows + .iter() + .map(|r| { + let s: String = sqlx::Row::get(r, "nutriments"); + serde_json::from_str(&s).unwrap() + }) + .collect(); + + let totals = tools::sum_nutriments(&entries_n); + // 200g of 150 kcal/100g = 300 kcal + assert_eq!(totals["energy_kcal"], json!(300.0)); + // 200g of 5g protein/100g = 10g + assert_eq!(totals["protein"], json!(10.0)); + } + + #[tokio::test] + async fn test_delete_entry() { + let db = TestDb::new().await; + + let result = sqlx::query( + r#"INSERT INTO entries (date, meal, food_name, grams, nutriments) + VALUES ('2026-01-15', 'breakfast', 'Test', 100.0, '{}')"#, + ) + .execute(&db.pool) + .await + .unwrap(); + let id = result.last_insert_rowid(); + + sqlx::query("DELETE FROM entries WHERE id = ?") + .bind(id) + .execute(&db.pool) + .await + .unwrap(); + + let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM entries") + .fetch_one(&db.pool) + .await + .unwrap(); + assert_eq!(count, 0); + } + + // ── Goals round trip ───────────────────────────────────────── + + #[tokio::test] + async fn test_get_and_update_goals() { + let db = TestDb::new().await; + + // Get default goals + let row = sqlx::query("SELECT goals FROM daily_goal WHERE id = 1") + .fetch_one(&db.pool) + .await + .unwrap(); + let goals_str: String = sqlx::Row::get(&row, "goals"); + let mut goals: Value = serde_json::from_str(&goals_str).unwrap(); + assert_eq!(goals["energy_kcal"], json!(2500)); + + // Update calories goal + if let Some(obj) = goals.as_object_mut() { + obj.insert("energy_kcal".to_string(), json!(2200)); + obj.insert("fructose".to_string(), json!(10)); + } + sqlx::query("UPDATE daily_goal SET goals = ?, updated_at = datetime('now') WHERE id = 1") + .bind(goals.to_string()) + .execute(&db.pool) + .await + .unwrap(); + + // Verify + let row = sqlx::query("SELECT goals FROM daily_goal WHERE id = 1") + .fetch_one(&db.pool) + .await + .unwrap(); + let goals_str: String = sqlx::Row::get(&row, "goals"); + let goals: Value = serde_json::from_str(&goals_str).unwrap(); + assert_eq!(goals["energy_kcal"], json!(2200)); + assert_eq!(goals["fructose"], json!(10)); + // Other goals preserved + assert_eq!(goals["protein"], json!(150)); + } + + // ── Weight round trip ───────────────────────────────────────── + + #[tokio::test] + async fn test_log_weight_and_history() { + let db = TestDb::new().await; + + sqlx::query("INSERT INTO weight_log (date, weight_kg) VALUES ('2026-01-10', 75.5)") + .execute(&db.pool) + .await + .unwrap(); + sqlx::query("INSERT INTO weight_log (date, weight_kg) VALUES ('2026-01-11', 75.3)") + .execute(&db.pool) + .await + .unwrap(); + sqlx::query("INSERT INTO weight_log (date, weight_kg) VALUES ('2026-01-12', 75.1)") + .execute(&db.pool) + .await + .unwrap(); + + let rows = sqlx::query("SELECT date, weight_kg FROM weight_log ORDER BY date") + .fetch_all(&db.pool) + .await + .unwrap(); + assert_eq!(rows.len(), 3); + + let w0: f64 = sqlx::Row::get(&rows[0], "weight_kg"); + let w2: f64 = sqlx::Row::get(&rows[2], "weight_kg"); + assert_eq!(w0, 75.5); + assert_eq!(w2, 75.1); + } + + #[tokio::test] + async fn test_weight_upsert_same_day() { + let db = TestDb::new().await; + + sqlx::query("INSERT INTO weight_log (date, weight_kg) VALUES ('2026-01-10', 75.5)") + .execute(&db.pool) + .await + .unwrap(); + // Same day, different weight — should upsert (in production; here just test insert doesn't fail) + sqlx::query( + r#"INSERT INTO weight_log (date, weight_kg) VALUES ('2026-01-10', 75.0) + ON CONFLICT DO UPDATE SET weight_kg = excluded.weight_kg"#, + ) + .execute(&db.pool) + .await + .unwrap(); + + let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM weight_log WHERE date = '2026-01-10'") + .fetch_one(&db.pool) + .await + .unwrap(); + assert_eq!(count, 1); + + let weight: f64 = sqlx::query_scalar("SELECT weight_kg FROM weight_log WHERE date = '2026-01-10'") + .fetch_one(&db.pool) + .await + .unwrap(); + assert_eq!(weight, 75.0); + } +} \ No newline at end of file