| 1 | use serde_json::json; |
| 2 | |
| 3 | use super::*; |
| 4 | use crate::search::index::{KeywordIndex, provisional_key}; |
| 5 | |
| 6 | const ROOM: &str = "!room:x"; |
| 7 | |
| 8 | fn message(id: &str, sender: &str, ts: u64, body: &str) -> Value { |
| 9 | json!({ |
| 10 | "type": "m.room.message", |
| 11 | "event_id": id, |
| 12 | "sender": sender, |
| 13 | "origin_server_ts": ts, |
| 14 | "content": { "msgtype": "m.text", "body": body }, |
| 15 | }) |
| 16 | } |
| 17 | |
| 18 | fn edit(id: &str, sender: &str, ts: u64, target: &str, body: &str) -> Value { |
| 19 | json!({ |
| 20 | "type": "m.room.message", |
| 21 | "event_id": id, |
| 22 | "sender": sender, |
| 23 | "origin_server_ts": ts, |
| 24 | "content": { |
| 25 | "msgtype": "m.text", |
| 26 | "body": format!("* {body}"), |
| 27 | "m.new_content": { "msgtype": "m.text", "body": body }, |
| 28 | "m.relates_to": { "rel_type": "m.replace", "event_id": target }, |
| 29 | }, |
| 30 | }) |
| 31 | } |
| 32 | |
| 33 | fn redaction(id: &str, target: &str) -> Value { |
| 34 | json!({ |
| 35 | "type": "m.room.redaction", |
| 36 | "event_id": id, |
| 37 | "sender": "@a:x", |
| 38 | "origin_server_ts": 99, |
| 39 | "redacts": target, |
| 40 | "content": {}, |
| 41 | }) |
| 42 | } |
| 43 | |
| 44 | fn feed(index: &mut KeywordIndex, redactions: &mut Redactions, value: Value) -> Effect { |
| 45 | apply(index, redactions, ROOM, observe(&value)) |
| 46 | } |
| 47 | |
| 48 | fn keys(index: &mut KeywordIndex, query: &str) -> Vec<String> { |
| 49 | let (hits, _) = index.search(query, None, None, 10).unwrap(); |
| 50 | hits.into_iter().map(|hit| hit.key).collect() |
| 51 | } |
| 52 | |
| 53 | /// backfill walks newest first, so an edit routinely lands before what it edits |
| 54 | #[test] |
| 55 | fn an_edit_before_its_original_keeps_the_edit_and_takes_the_original_timestamp() { |
| 56 | let mut index = KeywordIndex::new(false); |
| 57 | let mut redactions = Redactions::default(); |
| 58 | feed( |
| 59 | &mut index, |
| 60 | &mut redactions, |
| 61 | edit("$e", "@a:x", 20, "$o", "fixed words"), |
| 62 | ); |
| 63 | let provisional = index.get(&provisional_key("$o", "@a:x")).unwrap(); |
| 64 | assert!(provisional.provisional); |
| 65 | assert_eq!(provisional.ts, 20); |
| 66 | assert!(keys(&mut index, "fixed").is_empty()); |
| 67 | feed( |
| 68 | &mut index, |
| 69 | &mut redactions, |
| 70 | message("$o", "@a:x", 10, "typo wrds"), |
| 71 | ); |
| 72 | let doc = index.get("$o").unwrap(); |
| 73 | assert!(!doc.provisional); |
| 74 | assert_eq!((doc.ts, doc.content.as_str()), (10, "$e")); |
| 75 | assert_eq!(keys(&mut index, "fixed"), vec!["$o"]); |
| 76 | assert!(keys(&mut index, "wrds").is_empty()); |
| 77 | } |
| 78 | |
| 79 | #[test] |
| 80 | fn edits_from_someone_else_and_older_edits_lose() { |
| 81 | let mut index = KeywordIndex::new(false); |
| 82 | let mut redactions = Redactions::default(); |
| 83 | feed( |
| 84 | &mut index, |
| 85 | &mut redactions, |
| 86 | message("$o", "@a:x", 10, "original"), |
| 87 | ); |
| 88 | feed( |
| 89 | &mut index, |
| 90 | &mut redactions, |
| 91 | edit("$e2", "@a:x", 30, "$o", "newest"), |
| 92 | ); |
| 93 | assert_eq!( |
| 94 | feed( |
| 95 | &mut index, |
| 96 | &mut redactions, |
| 97 | edit("$e1", "@a:x", 20, "$o", "older") |
| 98 | ), |
| 99 | Effect::Nothing |
| 100 | ); |
| 101 | assert_eq!( |
| 102 | feed( |
| 103 | &mut index, |
| 104 | &mut redactions, |
| 105 | edit("$h", "@b:x", 40, "$o", "hijack") |
| 106 | ), |
| 107 | Effect::Nothing |
| 108 | ); |
| 109 | assert_eq!(keys(&mut index, "newest"), vec!["$o"]); |
| 110 | assert!(keys(&mut index, "hijack").is_empty()); |
| 111 | assert!(keys(&mut index, "original").is_empty()); |
| 112 | } |
| 113 | |
| 114 | #[test] |
| 115 | fn a_provisional_edit_from_someone_else_gives_way_to_the_original() { |
| 116 | let mut index = KeywordIndex::new(false); |
| 117 | let mut redactions = Redactions::default(); |
| 118 | feed( |
| 119 | &mut index, |
| 120 | &mut redactions, |
| 121 | edit("$h", "@b:x", 20, "$o", "hijack"), |
| 122 | ); |
| 123 | feed( |
| 124 | &mut index, |
| 125 | &mut redactions, |
| 126 | message("$o", "@a:x", 10, "genuine"), |
| 127 | ); |
| 128 | assert_eq!(keys(&mut index, "genuine"), vec!["$o"]); |
| 129 | assert!(keys(&mut index, "hijack").is_empty()); |
| 130 | } |
| 131 | |
| 132 | #[test] |
| 133 | fn a_redaction_removes_the_message_and_a_stale_copy_cannot_bring_it_back() { |
| 134 | let mut index = KeywordIndex::new(false); |
| 135 | let mut redactions = Redactions::default(); |
| 136 | feed( |
| 137 | &mut index, |
| 138 | &mut redactions, |
| 139 | message("$o", "@a:x", 10, "secret"), |
| 140 | ); |
| 141 | assert_eq!( |
| 142 | feed(&mut index, &mut redactions, redaction("$r", "$o")), |
| 143 | Effect::Removed |
| 144 | ); |
| 145 | feed( |
| 146 | &mut index, |
| 147 | &mut redactions, |
| 148 | message("$o", "@a:x", 10, "secret"), |
| 149 | ); |
| 150 | assert!(keys(&mut index, "secret").is_empty()); |
| 151 | } |
| 152 | |
| 153 | #[test] |
| 154 | fn redacting_the_latest_edit_asks_for_the_original_again() { |
| 155 | let mut index = KeywordIndex::new(false); |
| 156 | let mut redactions = Redactions::default(); |
| 157 | feed( |
| 158 | &mut index, |
| 159 | &mut redactions, |
| 160 | message("$o", "@a:x", 10, "first"), |
| 161 | ); |
| 162 | feed( |
| 163 | &mut index, |
| 164 | &mut redactions, |
| 165 | edit("$e", "@a:x", 20, "$o", "second"), |
| 166 | ); |
| 167 | assert_eq!( |
| 168 | feed(&mut index, &mut redactions, redaction("$r", "$e")), |
| 169 | Effect::Refetch("$o".to_owned()) |
| 170 | ); |
| 171 | let mut original = message("$o", "@a:x", 10, "first"); |
| 172 | original["unsigned"] = |
| 173 | json!({ "m.relations": { "m.replace": edit("$e", "@a:x", 20, "$o", "second") } }); |
| 174 | feed(&mut index, &mut redactions, original); |
| 175 | assert_eq!(keys(&mut index, "first"), vec!["$o"]); |
| 176 | assert!(keys(&mut index, "second").is_empty()); |
| 177 | } |
| 178 | |
| 179 | #[test] |
| 180 | fn reply_fallbacks_are_not_indexed() { |
| 181 | let mut index = KeywordIndex::new(false); |
| 182 | let mut redactions = Redactions::default(); |
| 183 | let mut reply = message("$r", "@a:x", 10, "> <@b:x> quoted stuff\n\nactual answer"); |
| 184 | reply["content"]["m.relates_to"] = json!({ "m.in_reply_to": { "event_id": "$q" } }); |
| 185 | feed(&mut index, &mut redactions, reply); |
| 186 | assert_eq!(keys(&mut index, "answer"), vec!["$r"]); |
| 187 | assert!(keys(&mut index, "quoted").is_empty()); |
| 188 | } |
| 189 | |
| 190 | #[test] |
| 191 | fn a_strangers_early_edit_cannot_shut_out_the_author() { |
| 192 | let mut index = KeywordIndex::new(false); |
| 193 | let mut redactions = Redactions::default(); |
| 194 | feed( |
| 195 | &mut index, |
| 196 | &mut redactions, |
| 197 | edit("$h", "@b:x", 30, "$o", "hijack"), |
| 198 | ); |
| 199 | feed( |
| 200 | &mut index, |
| 201 | &mut redactions, |
| 202 | edit("$e", "@a:x", 20, "$o", "genuine edit"), |
| 203 | ); |
| 204 | feed( |
| 205 | &mut index, |
| 206 | &mut redactions, |
| 207 | message("$o", "@a:x", 10, "original"), |
| 208 | ); |
| 209 | assert_eq!(keys(&mut index, "genuine"), vec!["$o"]); |
| 210 | assert!(keys(&mut index, "hijack").is_empty()); |
| 211 | assert!(index.get(&provisional_key("$o", "@b:x")).is_none()); |
| 212 | } |
| 213 | |
| 214 | #[test] |
| 215 | fn an_edit_of_an_edit_is_ignored() { |
| 216 | let mut index = KeywordIndex::new(false); |
| 217 | let mut redactions = Redactions::default(); |
| 218 | feed( |
| 219 | &mut index, |
| 220 | &mut redactions, |
| 221 | message("$o", "@a:x", 10, "original"), |
| 222 | ); |
| 223 | feed( |
| 224 | &mut index, |
| 225 | &mut redactions, |
| 226 | edit("$e", "@a:x", 20, "$o", "first"), |
| 227 | ); |
| 228 | assert_eq!( |
| 229 | feed( |
| 230 | &mut index, |
| 231 | &mut redactions, |
| 232 | edit("$ee", "@a:x", 30, "$e", "nested") |
| 233 | ), |
| 234 | Effect::Nothing |
| 235 | ); |
| 236 | assert!(index.get(&provisional_key("$e", "@a:x")).is_none()); |
| 237 | } |
| 238 | |
| 239 | #[test] |
| 240 | fn an_edit_to_unreadable_content_clears_the_old_text() { |
| 241 | let mut index = KeywordIndex::new(false); |
| 242 | let mut redactions = Redactions::default(); |
| 243 | feed( |
| 244 | &mut index, |
| 245 | &mut redactions, |
| 246 | message("$o", "@a:x", 10, "secret plans"), |
| 247 | ); |
| 248 | let mut blank = edit("$e", "@a:x", 20, "$o", "x"); |
| 249 | blank["content"]["m.new_content"] = json!({ "msgtype": "m.unknown" }); |
| 250 | feed(&mut index, &mut redactions, blank); |
| 251 | assert!(keys(&mut index, "secret").is_empty()); |
| 252 | } |