dk_protocol/
stale_overlay.rs1use chrono::{DateTime, Utc};
16use uuid::Uuid;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct CompetingChangeset {
22 pub changeset_id: Uuid,
23 pub session_id: Option<Uuid>,
24 pub state: String,
25 pub updated_at: DateTime<Utc>,
26}
27
28pub const LIVE_STATES: &[&str] = &["submitted", "verifying", "approved"];
37
38pub fn is_stale(
54 session_id: Uuid,
55 last_read: Option<DateTime<Utc>>,
56 competitors: &[CompetingChangeset],
57) -> Option<&CompetingChangeset> {
58 competitors.iter().find(|cs| {
59 if cs.session_id == Some(session_id) {
60 return false;
61 }
62 if !LIVE_STATES.contains(&cs.state.as_str()) {
63 return false;
64 }
65 match last_read {
66 Some(lr) => cs.updated_at > lr,
67 None => true,
68 }
69 })
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 fn cs(session: Option<Uuid>, state: &str, at: DateTime<Utc>) -> CompetingChangeset {
77 CompetingChangeset {
78 changeset_id: Uuid::new_v4(),
79 session_id: session,
80 state: state.to_string(),
81 updated_at: at,
82 }
83 }
84
85 #[test]
86 fn no_competitors_is_safe() {
87 let sid = Uuid::new_v4();
88 assert!(is_stale(sid, None, &[]).is_none());
89 assert!(is_stale(sid, Some(Utc::now()), &[]).is_none());
90 }
91
92 #[test]
93 fn self_session_never_stale() {
94 let sid = Uuid::new_v4();
95 let competitors = vec![cs(Some(sid), "submitted", Utc::now())];
96 assert!(is_stale(sid, None, &competitors).is_none());
97 }
98
99 #[test]
100 fn draft_never_stale() {
101 let sid = Uuid::new_v4();
102 let other = Uuid::new_v4();
103 let competitors = vec![cs(Some(other), "draft", Utc::now())];
104 assert!(is_stale(sid, None, &competitors).is_none());
105 }
106
107 #[test]
108 fn merged_never_stale() {
109 let sid = Uuid::new_v4();
111 let other = Uuid::new_v4();
112 let competitors = vec![cs(Some(other), "merged", Utc::now())];
113 assert!(is_stale(sid, None, &competitors).is_none());
114 }
115
116 #[test]
117 fn rejected_and_closed_are_inert() {
118 let sid = Uuid::new_v4();
119 let other = Uuid::new_v4();
120 assert!(is_stale(sid, None, &[cs(Some(other), "rejected", Utc::now())]).is_none());
121 assert!(is_stale(sid, None, &[cs(Some(other), "closed", Utc::now())]).is_none());
122 }
123
124 #[test]
125 fn competing_submitted_with_no_read_is_stale() {
126 let sid = Uuid::new_v4();
127 let other = Uuid::new_v4();
128 let competitors = vec![cs(Some(other), "submitted", Utc::now())];
129 assert!(is_stale(sid, None, &competitors).is_some());
130 }
131
132 #[test]
133 fn competing_verifying_counts_as_live() {
134 let sid = Uuid::new_v4();
135 let other = Uuid::new_v4();
136 let competitors = vec![cs(Some(other), "verifying", Utc::now())];
137 assert!(is_stale(sid, None, &competitors).is_some());
138 }
139
140 #[test]
141 fn competing_approved_counts_as_live() {
142 let sid = Uuid::new_v4();
143 let other = Uuid::new_v4();
144 let competitors = vec![cs(Some(other), "approved", Utc::now())];
145 assert!(is_stale(sid, None, &competitors).is_some());
146 }
147
148 #[test]
149 fn read_after_competing_submit_is_safe() {
150 let sid = Uuid::new_v4();
151 let other = Uuid::new_v4();
152 let submit_time = Utc::now() - chrono::Duration::seconds(10);
153 let read_time = Utc::now();
154 let competitors = vec![cs(Some(other), "submitted", submit_time)];
155 assert!(is_stale(sid, Some(read_time), &competitors).is_none());
156 }
157
158 #[test]
159 fn read_before_competing_submit_is_stale() {
160 let sid = Uuid::new_v4();
161 let other = Uuid::new_v4();
162 let read_time = Utc::now() - chrono::Duration::seconds(10);
163 let submit_time = Utc::now();
164 let competitors = vec![cs(Some(other), "submitted", submit_time)];
165 assert!(is_stale(sid, Some(read_time), &competitors).is_some());
166 }
167
168 #[test]
169 fn read_equal_to_competing_submit_is_safe() {
170 let sid = Uuid::new_v4();
172 let other = Uuid::new_v4();
173 let t = Utc::now();
174 let competitors = vec![cs(Some(other), "submitted", t)];
175 assert!(is_stale(sid, Some(t), &competitors).is_none());
176 }
177
178 #[test]
179 fn first_live_competitor_wins() {
180 let sid = Uuid::new_v4();
181 let other_a = Uuid::new_v4();
182 let other_b = Uuid::new_v4();
183 let t = Utc::now();
184 let draft = cs(Some(other_a), "draft", t);
186 let live = cs(Some(other_b), "submitted", t);
187 let competitors = vec![draft, live.clone()];
188 let got = is_stale(sid, None, &competitors).expect("expected stale");
189 assert_eq!(got.session_id, live.session_id);
190 assert_eq!(got.state, "submitted");
191 }
192
193 #[test]
194 fn no_session_id_still_counts() {
195 let sid = Uuid::new_v4();
198 let competitors = vec![cs(None, "submitted", Utc::now())];
199 assert!(is_stale(sid, None, &competitors).is_some());
200 }
201}