//! The context repository: spans in, days out. mod common; use chrono::NaiveDate; use goingson_core::{ContextKind, ContextRepository}; use goingson_db_sqlite::SqliteContextRepository; use rusqlite::params; use common::{create_test_user, setup_test_db}; fn day(date: &str) -> NaiveDate { date.parse().expect("a date") } #[test] fn a_context_is_authored_as_a_span_and_read_by_day() { let db = setup_test_db(); let user = create_test_user(&db); let repo = SqliteContextRepository::new(db.clone()); let leave = repo .create( user, "Leave", ContextKind::Vacation, day("2026-08-03"), day("2026-08-17"), ) .expect("the context is recorded"); assert_eq!(leave.days(), 15); // The read every view makes: one day, asked as a window of itself. for (date, inside) in [ ("2026-08-02", false), ("2026-08-03", true), ("2026-08-10", true), ("2026-08-17", true), ("2026-08-18", false), ] { let found = repo .list_overlapping(user, day(date), day(date)) .expect("the query answers"); assert_eq!( found.len(), usize::from(inside), "{date} should{} be inside", if inside { "" } else { " not" } ); } } #[test] fn a_window_finds_a_context_that_merely_overlaps_it() { let db = setup_test_db(); let user = create_test_user(&db); let repo = SqliteContextRepository::new(db.clone()); repo.create( user, "Leave", ContextKind::Vacation, day("2026-08-03"), day("2026-08-17"), ) .expect("recorded"); // A week strip asking about a week that starts inside the leave. let week = repo .list_overlapping(user, day("2026-08-10"), day("2026-08-16")) .expect("answers"); assert_eq!(week.len(), 1); // And one that ends the day it starts. let touching = repo .list_overlapping(user, day("2026-07-27"), day("2026-08-03")) .expect("answers"); assert_eq!(touching.len(), 1, "the first day counts"); let before = repo .list_overlapping(user, day("2026-07-20"), day("2026-08-02")) .expect("answers"); assert!(before.is_empty()); } #[test] fn one_persons_context_is_not_anothers() { let db = setup_test_db(); let me = create_test_user(&db); let you = create_test_user(&db); let repo = SqliteContextRepository::new(db.clone()); repo.create( me, "Leave", ContextKind::Vacation, day("2026-08-03"), day("2026-08-17"), ) .expect("recorded"); assert_eq!( repo.list_overlapping(you, day("2026-08-10"), day("2026-08-10")) .expect("answers") .len(), 0 ); } #[test] fn a_span_typed_backwards_is_straightened_rather_than_refused() { let db = setup_test_db(); let user = create_test_user(&db); let repo = SqliteContextRepository::new(db.clone()); let context = repo .create( user, "Trip", ContextKind::Trip, day("2026-08-17"), day("2026-08-03"), ) .expect("recorded"); assert_eq!(context.starts_on, day("2026-08-03")); assert_eq!(context.ends_on, day("2026-08-17")); assert!(context.covers(day("2026-08-10"))); } #[test] fn updating_moves_the_span_and_keeps_the_record() { let db = setup_test_db(); let user = create_test_user(&db); let repo = SqliteContextRepository::new(db.clone()); let context = repo .create( user, "Leave", ContextKind::Vacation, day("2026-08-03"), day("2026-08-17"), ) .expect("recorded"); let moved = repo .update( user, context.id, "Leave, extended", ContextKind::Vacation, day("2026-08-03"), day("2026-08-24"), ) .expect("updated"); assert_eq!(moved.id, context.id); assert_eq!(moved.label, "Leave, extended"); assert_eq!(moved.ends_on, day("2026-08-24")); assert!(moved.updated_at >= context.updated_at); } #[test] fn deleting_a_migrated_context_puts_its_event_back() { // The condition Max attached to accepting a migration that guesses: a // conversion is reversible per record, and reversing it is deleting the // context rather than reconstructing an event by hand. let db = setup_test_db(); let user = create_test_user(&db); let repo = SqliteContextRepository::new(db.clone()); let context = repo .create( user, "Conference", ContextKind::Other, day("2026-08-05"), day("2026-08-07"), ) .expect("recorded"); // Stand in for what migration 067 leaves behind: an event pointing at the // context it became, and a context pointing back. let event_id = uuid::Uuid::new_v4().to_string(); { let conn = db.conn().expect("a connection"); conn.execute( "INSERT INTO events (id, user_id, title, start_time, end_time, converted_to_context_id) VALUES (?1, ?2, 'Conference', '2026-08-05 14:00:00', '2026-08-07 17:00:00', ?3)", params![event_id, user.to_string(), context.id.to_string()], ) .expect("the hidden event"); conn.execute( "UPDATE contexts SET migrated_from_event_id = ?2 WHERE id = ?1", params![context.id.to_string(), event_id], ) .expect("the provenance mark"); } repo.delete(user, context.id).expect("deleted"); let conn = db.conn().expect("a connection"); let converted: Option = conn .query_row( "SELECT converted_to_context_id FROM events WHERE id = ?1", params![event_id], |row| row.get(0), ) .expect("the event is still there"); assert!( converted.is_none(), "the event is on the timeline again, with its times" ); } #[test] fn deleting_someone_elses_context_is_a_not_found() { let db = setup_test_db(); let me = create_test_user(&db); let you = create_test_user(&db); let repo = SqliteContextRepository::new(db.clone()); let context = repo .create( me, "Leave", ContextKind::Vacation, day("2026-08-03"), day("2026-08-17"), ) .expect("recorded"); assert!(repo.delete(you, context.id).is_err()); assert_eq!(repo.list_all(me).expect("answers").len(), 1); } #[test] fn every_kind_round_trips_through_the_store() { let db = setup_test_db(); let user = create_test_user(&db); let repo = SqliteContextRepository::new(db.clone()); for (i, kind) in [ ContextKind::Vacation, ContextKind::Trip, ContextKind::Illness, ContextKind::Sprint, ContextKind::Other, ] .into_iter() .enumerate() { let date = day("2026-09-01") + chrono::Duration::days(i as i64 * 2); repo.create(user, kind.as_str(), kind, date, date) .expect("recorded"); } let all = repo.list_all(user).expect("answers"); assert_eq!(all.len(), 5); for context in all { assert_eq!( context.kind.as_str(), context.label, "the kind survived the round trip" ); } }