Files
k-mood/crates/adapters/sqlite/tests/weather_dimension_test.rs
Gabriel Kaszewski 23d052278a
All checks were successful
CI / ci (push) Successful in 19m38s
changes
2026-08-26 20:58:14 +02:00

295 lines
8.7 KiB
Rust

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<DimensionValue> {
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);
}