use sqlx::sqlite::SqlitePoolOptions; use domain::dimension::{DimensionKind, DimensionValue}; use domain::entry::{Mood, MoodEntry, MoodEntryId}; use domain::location::Coordinates; use domain::ports::{EntryDimensionPort, MoodEntryCommandPort, UserCommandPort}; use domain::provider::ProviderName; use domain::testing::test_user; use domain::weather::{Celsius, Condition, Weather}; use sqlite::repositories::{ SqliteEntryCommandRepository, SqliteLocationDimensionRepository, SqliteUserCommandRepository, SqliteWeatherDimensionRepository, }; async fn an_entry() -> (sqlx::SqlitePool, MoodEntryId) { let pool = SqlitePoolOptions::new() .max_connections(1) .connect("sqlite::memory:") .await .unwrap(); sqlite::run_migrations(&pool).await.unwrap(); let user = test_user("alice"); SqliteUserCommandRepository::new(pool.clone()) .save(&user) .await .unwrap(); let entry = MoodEntry::new( user.id().clone(), Mood::Good, chrono::DateTime::parse_from_rfc3339("2026-08-20T14:00:00+02:00").unwrap(), ); SqliteEntryCommandRepository::new(pool.clone()) .save(&entry) .await .unwrap(); (pool, entry.id().clone()) } fn observed(condition: Condition, degrees: f64) -> DimensionValue { DimensionValue::Weather(Weather::new( condition, Celsius::new(degrees).unwrap(), ProviderName::new("open-meteo").unwrap(), )) } async fn weather_of(pool: &sqlx::SqlitePool, entry_id: &MoodEntryId) -> Option { SqliteWeatherDimensionRepository::new(pool.clone()) .load(std::slice::from_ref(entry_id)) .await .unwrap() .remove(entry_id) } #[tokio::test] async fn observed_weather_is_stored_and_read_back_whole() { let (pool, entry_id) = an_entry().await; let weather = SqliteWeatherDimensionRepository::new(pool.clone()); weather .save(&entry_id, &[observed(Condition::Rain, 11.5)]) .await .unwrap(); let Some(DimensionValue::Weather(stored)) = weather_of(&pool, &entry_id).await else { panic!("the weather was not stored"); }; assert_eq!(stored.condition(), Condition::Rain); assert!((stored.temperature().value() - 11.5).abs() < f64::EPSILON); assert_eq!(stored.observed_by().value(), "open-meteo"); } #[tokio::test] async fn editing_an_entry_cannot_erase_what_a_provider_observed() { let (pool, entry_id) = an_entry().await; let weather = SqliteWeatherDimensionRepository::new(pool.clone()); weather .save(&entry_id, &[observed(Condition::Rain, 11.5)]) .await .unwrap(); let an_edit_that_says_nothing_about_weather = vec![DimensionValue::Location( Coordinates::new(52.2297, 21.0122).unwrap(), )]; weather .save(&entry_id, &an_edit_that_says_nothing_about_weather) .await .unwrap(); assert!( weather_of(&pool, &entry_id).await.is_some(), "weather is observed, not stated: an edit that omits it is not a request to delete it" ); } #[tokio::test] async fn a_later_observation_replaces_an_earlier_one() { let (pool, entry_id) = an_entry().await; let weather = SqliteWeatherDimensionRepository::new(pool.clone()); weather .save(&entry_id, &[observed(Condition::Clear, 20.0)]) .await .unwrap(); weather .save(&entry_id, &[observed(Condition::Snow, -2.0)]) .await .unwrap(); let Some(DimensionValue::Weather(stored)) = weather_of(&pool, &entry_id).await else { panic!("the weather was lost"); }; assert_eq!(stored.condition(), Condition::Snow); let rows: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM entry_weather") .fetch_one(&pool) .await .unwrap(); assert_eq!(rows.0, 1); } #[tokio::test] async fn a_condition_this_build_does_not_know_is_skipped() { let (pool, entry_id) = an_entry().await; sqlx::query( "INSERT INTO entry_weather (entry_id, condition, temperature, observed_by) VALUES (?, 'raining frogs', 11.5, 'open-meteo')", ) .bind(entry_id.value().to_string()) .execute(&pool) .await .unwrap(); assert!(weather_of(&pool, &entry_id).await.is_none()); } #[tokio::test] async fn deleting_an_entry_takes_its_weather_with_it() { let (pool, entry_id) = an_entry().await; SqliteWeatherDimensionRepository::new(pool.clone()) .save(&entry_id, &[observed(Condition::Rain, 11.5)]) .await .unwrap(); SqliteEntryCommandRepository::new(pool.clone()) .delete(&entry_id) .await .unwrap(); let rows: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM entry_weather") .fetch_one(&pool) .await .unwrap(); assert_eq!(rows.0, 0); } #[tokio::test] async fn weather_reports_its_own_kind() { assert_eq!(observed(Condition::Fog, 3.0).kind(), DimensionKind::Weather); let _ = SqliteLocationDimensionRepository::new; } #[tokio::test] async fn a_place_with_no_weather_is_backlogged_and_then_is_not() { use domain::ports::WeatherBacklogQueryPort; use sqlite::repositories::SqliteWeatherBacklogRepository; let (pool, entry_id) = an_entry().await; SqliteLocationDimensionRepository::new(pool.clone()) .save( &entry_id, &[DimensionValue::Location( Coordinates::new(52.2297, 21.0122).unwrap(), )], ) .await .unwrap(); let backlog = SqliteWeatherBacklogRepository::new(pool.clone()); let waiting = backlog.find_places_without_weather(50).await.unwrap(); assert_eq!(waiting.len(), 1); assert_eq!(waiting[0].entry_id, entry_id); assert!((waiting[0].coordinates.latitude().value() - 52.2297).abs() < 1e-9); SqliteWeatherDimensionRepository::new(pool.clone()) .save(&entry_id, &[observed(Condition::Rain, 11.5)]) .await .unwrap(); assert!( backlog .find_places_without_weather(50) .await .unwrap() .is_empty(), "an entry that now has weather is no longer waiting for any" ); } #[tokio::test] async fn only_entries_that_know_where_they_were_are_backlogged() { use domain::ports::WeatherBacklogQueryPort; use sqlite::repositories::SqliteWeatherBacklogRepository; let (pool, somewhere_known) = an_entry().await; SqliteLocationDimensionRepository::new(pool.clone()) .save( &somewhere_known, &[DimensionValue::Location( Coordinates::new(52.2297, 21.0122).unwrap(), )], ) .await .unwrap(); let owner: (String,) = sqlx::query_as("SELECT id FROM users LIMIT 1") .fetch_one(&pool) .await .unwrap(); let nowhere = MoodEntry::new( domain::user::UserId::from_uuid(owner.0.parse().unwrap()), Mood::Meh, chrono::DateTime::parse_from_rfc3339("2026-08-21T09:00:00+02:00").unwrap(), ); SqliteEntryCommandRepository::new(pool.clone()) .save(&nowhere) .await .unwrap(); let waiting = SqliteWeatherBacklogRepository::new(pool) .find_places_without_weather(50) .await .unwrap(); let ids: Vec<&MoodEntryId> = waiting.iter().map(|place| &place.entry_id).collect(); assert_eq!( ids, [&somewhere_known], "weather needs somewhere to have happened" ); } #[tokio::test] async fn the_backlog_is_bounded_by_what_is_asked_for() { use domain::ports::WeatherBacklogQueryPort; use sqlite::repositories::SqliteWeatherBacklogRepository; let (pool, first) = an_entry().await; let locations = SqliteLocationDimensionRepository::new(pool.clone()); let somewhere = DimensionValue::Location(Coordinates::new(52.2297, 21.0122).unwrap()); locations .save(&first, std::slice::from_ref(&somewhere)) .await .unwrap(); let owner: (String,) = sqlx::query_as("SELECT id FROM users LIMIT 1") .fetch_one(&pool) .await .unwrap(); for hour in 0..5 { let entry = MoodEntry::new( domain::user::UserId::from_uuid(owner.0.parse().unwrap()), Mood::Good, chrono::DateTime::parse_from_rfc3339(&format!("2026-08-2{hour}T09:00:00+02:00")) .unwrap(), ); SqliteEntryCommandRepository::new(pool.clone()) .save(&entry) .await .unwrap(); locations .save(entry.id(), std::slice::from_ref(&somewhere)) .await .unwrap(); } let waiting = SqliteWeatherBacklogRepository::new(pool) .find_places_without_weather(3) .await .unwrap(); assert_eq!(waiting.len(), 3); }