Skip to main content

max / goingson

7.3 KB · 269 lines History Blame Raw
1 //! The context repository: spans in, days out.
2
3 mod common;
4
5 use chrono::NaiveDate;
6 use goingson_core::{ContextKind, ContextRepository};
7 use goingson_db_sqlite::SqliteContextRepository;
8 use rusqlite::params;
9
10 use common::{create_test_user, setup_test_db};
11
12 fn day(date: &str) -> NaiveDate {
13 date.parse().expect("a date")
14 }
15
16 #[test]
17 fn a_context_is_authored_as_a_span_and_read_by_day() {
18 let db = setup_test_db();
19 let user = create_test_user(&db);
20 let repo = SqliteContextRepository::new(db.clone());
21
22 let leave = repo
23 .create(
24 user,
25 "Leave",
26 ContextKind::Vacation,
27 day("2026-08-03"),
28 day("2026-08-17"),
29 )
30 .expect("the context is recorded");
31 assert_eq!(leave.days(), 15);
32
33 // The read every view makes: one day, asked as a window of itself.
34 for (date, inside) in [
35 ("2026-08-02", false),
36 ("2026-08-03", true),
37 ("2026-08-10", true),
38 ("2026-08-17", true),
39 ("2026-08-18", false),
40 ] {
41 let found = repo
42 .list_overlapping(user, day(date), day(date))
43 .expect("the query answers");
44 assert_eq!(
45 found.len(),
46 usize::from(inside),
47 "{date} should{} be inside",
48 if inside { "" } else { " not" }
49 );
50 }
51 }
52
53 #[test]
54 fn a_window_finds_a_context_that_merely_overlaps_it() {
55 let db = setup_test_db();
56 let user = create_test_user(&db);
57 let repo = SqliteContextRepository::new(db.clone());
58
59 repo.create(
60 user,
61 "Leave",
62 ContextKind::Vacation,
63 day("2026-08-03"),
64 day("2026-08-17"),
65 )
66 .expect("recorded");
67
68 // A week strip asking about a week that starts inside the leave.
69 let week = repo
70 .list_overlapping(user, day("2026-08-10"), day("2026-08-16"))
71 .expect("answers");
72 assert_eq!(week.len(), 1);
73
74 // And one that ends the day it starts.
75 let touching = repo
76 .list_overlapping(user, day("2026-07-27"), day("2026-08-03"))
77 .expect("answers");
78 assert_eq!(touching.len(), 1, "the first day counts");
79
80 let before = repo
81 .list_overlapping(user, day("2026-07-20"), day("2026-08-02"))
82 .expect("answers");
83 assert!(before.is_empty());
84 }
85
86 #[test]
87 fn one_persons_context_is_not_anothers() {
88 let db = setup_test_db();
89 let me = create_test_user(&db);
90 let you = create_test_user(&db);
91 let repo = SqliteContextRepository::new(db.clone());
92
93 repo.create(
94 me,
95 "Leave",
96 ContextKind::Vacation,
97 day("2026-08-03"),
98 day("2026-08-17"),
99 )
100 .expect("recorded");
101
102 assert_eq!(
103 repo.list_overlapping(you, day("2026-08-10"), day("2026-08-10"))
104 .expect("answers")
105 .len(),
106 0
107 );
108 }
109
110 #[test]
111 fn a_span_typed_backwards_is_straightened_rather_than_refused() {
112 let db = setup_test_db();
113 let user = create_test_user(&db);
114 let repo = SqliteContextRepository::new(db.clone());
115
116 let context = repo
117 .create(
118 user,
119 "Trip",
120 ContextKind::Trip,
121 day("2026-08-17"),
122 day("2026-08-03"),
123 )
124 .expect("recorded");
125
126 assert_eq!(context.starts_on, day("2026-08-03"));
127 assert_eq!(context.ends_on, day("2026-08-17"));
128 assert!(context.covers(day("2026-08-10")));
129 }
130
131 #[test]
132 fn updating_moves_the_span_and_keeps_the_record() {
133 let db = setup_test_db();
134 let user = create_test_user(&db);
135 let repo = SqliteContextRepository::new(db.clone());
136
137 let context = repo
138 .create(
139 user,
140 "Leave",
141 ContextKind::Vacation,
142 day("2026-08-03"),
143 day("2026-08-17"),
144 )
145 .expect("recorded");
146
147 let moved = repo
148 .update(
149 user,
150 context.id,
151 "Leave, extended",
152 ContextKind::Vacation,
153 day("2026-08-03"),
154 day("2026-08-24"),
155 )
156 .expect("updated");
157
158 assert_eq!(moved.id, context.id);
159 assert_eq!(moved.label, "Leave, extended");
160 assert_eq!(moved.ends_on, day("2026-08-24"));
161 assert!(moved.updated_at >= context.updated_at);
162 }
163
164 #[test]
165 fn deleting_a_migrated_context_puts_its_event_back() {
166 // The condition Max attached to accepting a migration that guesses: a
167 // conversion is reversible per record, and reversing it is deleting the
168 // context rather than reconstructing an event by hand.
169 let db = setup_test_db();
170 let user = create_test_user(&db);
171 let repo = SqliteContextRepository::new(db.clone());
172
173 let context = repo
174 .create(
175 user,
176 "Conference",
177 ContextKind::Other,
178 day("2026-08-05"),
179 day("2026-08-07"),
180 )
181 .expect("recorded");
182
183 // Stand in for what migration 067 leaves behind: an event pointing at the
184 // context it became, and a context pointing back.
185 let event_id = uuid::Uuid::new_v4().to_string();
186 {
187 let conn = db.conn().expect("a connection");
188 conn.execute(
189 "INSERT INTO events (id, user_id, title, start_time, end_time, converted_to_context_id)
190 VALUES (?1, ?2, 'Conference', '2026-08-05 14:00:00', '2026-08-07 17:00:00', ?3)",
191 params![event_id, user.to_string(), context.id.to_string()],
192 )
193 .expect("the hidden event");
194 conn.execute(
195 "UPDATE contexts SET migrated_from_event_id = ?2 WHERE id = ?1",
196 params![context.id.to_string(), event_id],
197 )
198 .expect("the provenance mark");
199 }
200
201 repo.delete(user, context.id).expect("deleted");
202
203 let conn = db.conn().expect("a connection");
204 let converted: Option<String> = conn
205 .query_row(
206 "SELECT converted_to_context_id FROM events WHERE id = ?1",
207 params![event_id],
208 |row| row.get(0),
209 )
210 .expect("the event is still there");
211 assert!(
212 converted.is_none(),
213 "the event is on the timeline again, with its times"
214 );
215 }
216
217 #[test]
218 fn deleting_someone_elses_context_is_a_not_found() {
219 let db = setup_test_db();
220 let me = create_test_user(&db);
221 let you = create_test_user(&db);
222 let repo = SqliteContextRepository::new(db.clone());
223
224 let context = repo
225 .create(
226 me,
227 "Leave",
228 ContextKind::Vacation,
229 day("2026-08-03"),
230 day("2026-08-17"),
231 )
232 .expect("recorded");
233
234 assert!(repo.delete(you, context.id).is_err());
235 assert_eq!(repo.list_all(me).expect("answers").len(), 1);
236 }
237
238 #[test]
239 fn every_kind_round_trips_through_the_store() {
240 let db = setup_test_db();
241 let user = create_test_user(&db);
242 let repo = SqliteContextRepository::new(db.clone());
243
244 for (i, kind) in [
245 ContextKind::Vacation,
246 ContextKind::Trip,
247 ContextKind::Illness,
248 ContextKind::Sprint,
249 ContextKind::Other,
250 ]
251 .into_iter()
252 .enumerate()
253 {
254 let date = day("2026-09-01") + chrono::Duration::days(i as i64 * 2);
255 repo.create(user, kind.as_str(), kind, date, date)
256 .expect("recorded");
257 }
258
259 let all = repo.list_all(user).expect("answers");
260 assert_eq!(all.len(), 5);
261 for context in all {
262 assert_eq!(
263 context.kind.as_str(),
264 context.label,
265 "the kind survived the round trip"
266 );
267 }
268 }
269