report.rs (15018B)
1 #![cfg(feature = "serde_json")] 2 3 use radroots_events::{ 4 kinds::{KIND_ARTICLE, KIND_POST, KIND_REPORT}, 5 report::RadrootsReport, 6 social::{RadrootsReportFileTarget, RadrootsReportType, RadrootsSocialTarget}, 7 tags::{TAG_A, TAG_E, TAG_MAGNET, TAG_P, TAG_SERVER, TAG_SHA256}, 8 }; 9 use radroots_events_codec::{ 10 error::{EventEncodeError, EventParseError}, 11 report::{ 12 decode::{data_from_event, parsed_from_event, report_from_event}, 13 encode::{report_build_tags, to_wire_parts, to_wire_parts_with_kind}, 14 }, 15 }; 16 17 const EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 18 const REPORTED: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; 19 const FILE_HASH: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; 20 const ARTICLE_D_TAG: &str = "DDDDDDDDDDDDDDDDDDDDDA"; 21 22 fn profile_report() -> RadrootsReport { 23 RadrootsReport { 24 reported_pubkey: REPORTED.to_string(), 25 report_type: RadrootsReportType::Spam, 26 event: None, 27 file: None, 28 content: None, 29 } 30 } 31 32 fn event_report() -> RadrootsReport { 33 RadrootsReport { 34 reported_pubkey: REPORTED.to_string(), 35 report_type: RadrootsReportType::Illegal, 36 event: Some(RadrootsSocialTarget::Event { 37 id: EVENT_ID.to_string(), 38 author: Some(REPORTED.to_string()), 39 event_kind: Some(KIND_POST), 40 relays: Some(vec!["wss://relay.example.test".to_string()]), 41 }), 42 file: None, 43 content: Some("Contains prohibited listing text".to_string()), 44 } 45 } 46 47 fn file_report() -> RadrootsReport { 48 RadrootsReport { 49 reported_pubkey: REPORTED.to_string(), 50 report_type: RadrootsReportType::Malware, 51 event: None, 52 file: Some(RadrootsReportFileTarget { 53 sha256: Some(FILE_HASH.to_string()), 54 url: Some("https://media.example.test/blob".to_string()), 55 magnet: Some("magnet:?xt=urn:btih:example".to_string()), 56 }), 57 content: None, 58 } 59 } 60 61 fn address_report() -> RadrootsReport { 62 RadrootsReport { 63 reported_pubkey: REPORTED.to_string(), 64 report_type: RadrootsReportType::Nudity, 65 event: Some(RadrootsSocialTarget::Address { 66 address: format!("{KIND_ARTICLE}:{REPORTED}:{ARTICLE_D_TAG}"), 67 author: Some(REPORTED.to_string()), 68 event_kind: Some(KIND_ARTICLE), 69 relays: None, 70 }), 71 file: None, 72 content: None, 73 } 74 } 75 76 fn has_tag(tags: &[Vec<String>], key: &str, value: &str) -> bool { 77 tags.iter().any(|tag| { 78 tag.first().map(|entry| entry.as_str()) == Some(key) 79 && tag.get(1).map(|entry| entry.as_str()) == Some(value) 80 }) 81 } 82 83 fn replace_tag_value(tags: &mut [Vec<String>], key: &str, value: &str) { 84 let tag = tags 85 .iter_mut() 86 .find(|tag| tag.first().map(String::as_str) == Some(key)) 87 .expect("tag"); 88 tag[1] = value.to_string(); 89 } 90 91 #[test] 92 fn report_to_wire_parts_roundtrips_pubkey_event_and_file_reports() { 93 let profile = to_wire_parts(&profile_report()).unwrap(); 94 assert_eq!(profile.kind, KIND_REPORT); 95 assert!(profile.content.is_empty()); 96 assert!(has_tag(&profile.tags, TAG_P, REPORTED)); 97 assert_eq!( 98 profile.tags[0].get(2).map(|value| value.as_str()), 99 Some("spam") 100 ); 101 let decoded = report_from_event(profile.kind, &profile.tags, &profile.content).unwrap(); 102 assert_eq!(decoded.reported_pubkey, REPORTED); 103 assert_eq!(decoded.report_type, RadrootsReportType::Spam); 104 105 let event = to_wire_parts(&event_report()).unwrap(); 106 assert_eq!(event.content, "Contains prohibited listing text"); 107 assert!(has_tag(&event.tags, TAG_E, EVENT_ID)); 108 let decoded = report_from_event(event.kind, &event.tags, &event.content).unwrap(); 109 assert!(matches!( 110 decoded.event, 111 Some(RadrootsSocialTarget::Event { .. }) 112 )); 113 assert_eq!(decoded.report_type, RadrootsReportType::Illegal); 114 115 let file = to_wire_parts(&file_report()).unwrap(); 116 assert!(has_tag(&file.tags, TAG_SHA256, FILE_HASH)); 117 assert!(has_tag( 118 &file.tags, 119 TAG_SERVER, 120 "https://media.example.test/blob" 121 )); 122 assert!(has_tag( 123 &file.tags, 124 TAG_MAGNET, 125 "magnet:?xt=urn:btih:example" 126 )); 127 let decoded = report_from_event(file.kind, &file.tags, &file.content).unwrap(); 128 assert_eq!( 129 decoded.file.and_then(|target| target.sha256).as_deref(), 130 Some(FILE_HASH) 131 ); 132 133 let address = to_wire_parts(&address_report()).unwrap(); 134 assert!(has_tag( 135 &address.tags, 136 TAG_A, 137 format!("{KIND_ARTICLE}:{REPORTED}:{ARTICLE_D_TAG}").as_str() 138 )); 139 let decoded = report_from_event(address.kind, &address.tags, &address.content).unwrap(); 140 assert_eq!(decoded.report_type, RadrootsReportType::Nudity); 141 assert!(matches!( 142 decoded.event, 143 Some(RadrootsSocialTarget::Address { relays: None, .. }) 144 )); 145 } 146 147 #[test] 148 fn report_codec_rejects_missing_pubkey_unknown_type_bad_hash_and_wrong_kind() { 149 let mut report = profile_report(); 150 report.reported_pubkey = " ".to_string(); 151 assert!(matches!( 152 report_build_tags(&report), 153 Err(EventEncodeError::EmptyRequiredField("reported_pubkey")) 154 )); 155 156 let mut report = profile_report(); 157 report.reported_pubkey = "not-a-pubkey".to_string(); 158 assert!(matches!( 159 report_build_tags(&report), 160 Err(EventEncodeError::InvalidField("reported_pubkey")) 161 )); 162 163 assert!(matches!( 164 to_wire_parts_with_kind(&profile_report(), KIND_POST), 165 Err(EventEncodeError::InvalidKind(KIND_POST)) 166 )); 167 168 let mut report = file_report(); 169 report.file.as_mut().unwrap().sha256 = Some("bad".to_string()); 170 assert!(matches!( 171 to_wire_parts(&report), 172 Err(EventEncodeError::InvalidField("file.sha256")) 173 )); 174 175 let mut report = file_report(); 176 report.file = Some(RadrootsReportFileTarget { 177 sha256: None, 178 url: None, 179 magnet: None, 180 }); 181 assert!(matches!( 182 to_wire_parts(&report), 183 Err(EventEncodeError::EmptyRequiredField("file")) 184 )); 185 186 let mut report = file_report(); 187 report.file.as_mut().unwrap().url = Some("ftp://media.example.test/blob".to_string()); 188 assert!(matches!( 189 to_wire_parts(&report), 190 Err(EventEncodeError::InvalidField("file.url")) 191 )); 192 193 let mut report = file_report(); 194 report.file.as_mut().unwrap().magnet = Some(" ".to_string()); 195 assert!(matches!( 196 to_wire_parts(&report), 197 Err(EventEncodeError::EmptyRequiredField("file.magnet")) 198 )); 199 200 let err = report_from_event(KIND_REPORT, &[], "").unwrap_err(); 201 assert!(matches!(err, EventParseError::MissingTag(TAG_P))); 202 203 let tags = vec![vec![ 204 TAG_P.to_string(), 205 "not-a-pubkey".to_string(), 206 "spam".to_string(), 207 ]]; 208 let err = report_from_event(KIND_REPORT, &tags, "").unwrap_err(); 209 assert!(matches!(err, EventParseError::InvalidTag(TAG_P))); 210 211 let tags = vec![vec![ 212 TAG_P.to_string(), 213 REPORTED.to_string(), 214 "unknown".to_string(), 215 ]]; 216 let err = report_from_event(KIND_REPORT, &tags, "").unwrap_err(); 217 assert!(matches!(err, EventParseError::InvalidTag(TAG_P))); 218 219 let tags = vec![ 220 vec![ 221 TAG_P.to_string(), 222 REPORTED.to_string(), 223 "malware".to_string(), 224 ], 225 vec![ 226 TAG_SHA256.to_string(), 227 "bad".to_string(), 228 "malware".to_string(), 229 ], 230 ]; 231 let err = report_from_event(KIND_REPORT, &tags, "").unwrap_err(); 232 assert!(matches!(err, EventParseError::InvalidTag(TAG_SHA256))); 233 234 let err = report_from_event(KIND_POST, &tags, "").unwrap_err(); 235 assert!(matches!( 236 err, 237 EventParseError::InvalidKind { 238 expected: "1984", 239 got: KIND_POST 240 } 241 )); 242 } 243 244 #[test] 245 fn report_codec_rejects_bad_event_targets_and_report_type_mismatches() { 246 let mut report = event_report(); 247 report.event = Some(RadrootsSocialTarget::External { 248 id: "https://example.test/report".to_string(), 249 external_kind: "web".to_string(), 250 hint: None, 251 }); 252 assert!(matches!( 253 report_build_tags(&report), 254 Err(EventEncodeError::InvalidField("event")) 255 )); 256 257 let mut report = event_report(); 258 if let Some(RadrootsSocialTarget::Event { id, .. }) = &mut report.event { 259 *id = "not-a-lowercase-hex-id".to_string(); 260 } 261 assert!(matches!( 262 report_build_tags(&report), 263 Err(EventEncodeError::InvalidField("event.id")) 264 )); 265 266 let mut report = address_report(); 267 if let Some(RadrootsSocialTarget::Address { 268 address, relays, .. 269 }) = &mut report.event 270 { 271 *address = "not-an-address".to_string(); 272 *relays = Some(vec![ 273 " ".to_string(), 274 "wss://relay.example.test".to_string(), 275 ]); 276 } 277 assert!(matches!( 278 report_build_tags(&report), 279 Err(EventEncodeError::InvalidField("event.address")) 280 )); 281 282 let tags = vec![ 283 vec![TAG_P.to_string(), REPORTED.to_string(), "spam".to_string()], 284 vec![ 285 TAG_E.to_string(), 286 EVENT_ID.to_string(), 287 "illegal".to_string(), 288 ], 289 ]; 290 let err = report_from_event(KIND_REPORT, &tags, "").unwrap_err(); 291 assert!(matches!(err, EventParseError::InvalidTag(TAG_E))); 292 293 let tags = vec![ 294 vec![TAG_P.to_string(), REPORTED.to_string(), "spam".to_string()], 295 vec![ 296 TAG_A.to_string(), 297 "bad-address".to_string(), 298 "spam".to_string(), 299 ], 300 ]; 301 let err = report_from_event(KIND_REPORT, &tags, "").unwrap_err(); 302 assert!(matches!(err, EventParseError::InvalidNumber(TAG_A, _))); 303 304 let mut tags = report_build_tags(&address_report()).unwrap(); 305 let address = tags 306 .iter_mut() 307 .find(|tag| tag.first().map(String::as_str) == Some(TAG_A)) 308 .expect("address tag"); 309 address.truncate(1); 310 let err = report_from_event(KIND_REPORT, &tags, "").unwrap_err(); 311 assert!(matches!(err, EventParseError::InvalidTag(TAG_A))); 312 313 let mut tags = report_build_tags(&event_report()).unwrap(); 314 let event = tags 315 .iter_mut() 316 .find(|tag| tag.first().map(String::as_str) == Some(TAG_E)) 317 .expect("event tag"); 318 event.truncate(1); 319 let err = report_from_event(KIND_REPORT, &tags, "").unwrap_err(); 320 assert!(matches!(err, EventParseError::InvalidTag(TAG_E))); 321 322 let mut tags = report_build_tags(&address_report()).unwrap(); 323 let address = tags 324 .iter_mut() 325 .find(|tag| tag.first().map(String::as_str) == Some(TAG_A)) 326 .expect("address tag"); 327 address.truncate(2); 328 let decoded = report_from_event(KIND_REPORT, &tags, "").expect("address report"); 329 assert!(matches!( 330 decoded.event, 331 Some(RadrootsSocialTarget::Address { .. }) 332 )); 333 } 334 335 #[test] 336 fn report_codec_covers_report_type_and_file_variants() { 337 for (report_type, expected) in [ 338 (RadrootsReportType::Profanity, "profanity"), 339 (RadrootsReportType::Impersonation, "impersonation"), 340 (RadrootsReportType::Other, "other"), 341 ] { 342 let mut report = profile_report(); 343 report.report_type = report_type; 344 let parts = to_wire_parts(&report).unwrap(); 345 assert_eq!(parts.tags[0].get(2).map(String::as_str), Some(expected)); 346 let decoded = report_from_event(parts.kind, &parts.tags, "").unwrap(); 347 assert_eq!(decoded.report_type, report.report_type); 348 } 349 350 let mut report = file_report(); 351 report.file = Some(RadrootsReportFileTarget { 352 sha256: None, 353 url: Some("https://media.example.test/blob".to_string()), 354 magnet: None, 355 }); 356 let parts = to_wire_parts(&report).unwrap(); 357 let decoded = report_from_event(parts.kind, &parts.tags, "").unwrap(); 358 assert_eq!( 359 decoded.file.and_then(|file| file.url).as_deref(), 360 Some("https://media.example.test/blob") 361 ); 362 363 let mut report = file_report(); 364 report.file = Some(RadrootsReportFileTarget { 365 sha256: None, 366 url: None, 367 magnet: Some("magnet:?xt=urn:btih:example".to_string()), 368 }); 369 let parts = to_wire_parts(&report).unwrap(); 370 let decoded = report_from_event(parts.kind, &parts.tags, "").unwrap(); 371 assert_eq!( 372 decoded.file.and_then(|file| file.magnet).as_deref(), 373 Some("magnet:?xt=urn:btih:example") 374 ); 375 376 let mut report = event_report(); 377 if let Some(RadrootsSocialTarget::Event { relays, .. }) = &mut report.event { 378 *relays = None; 379 } 380 let parts = to_wire_parts(&report).unwrap(); 381 let event = parts 382 .tags 383 .iter() 384 .find(|tag| tag.first().map(String::as_str) == Some(TAG_E)) 385 .expect("event tag"); 386 assert_eq!(event.len(), 3); 387 388 let mut report = address_report(); 389 if let Some(RadrootsSocialTarget::Address { relays, .. }) = &mut report.event { 390 *relays = Some(vec![ 391 " ".to_string(), 392 "wss://relay.example.test".to_string(), 393 ]); 394 } 395 let parts = to_wire_parts(&report).unwrap(); 396 let address = parts 397 .tags 398 .iter() 399 .find(|tag| tag.first().map(String::as_str) == Some(TAG_A)) 400 .expect("address tag"); 401 assert!( 402 address 403 .iter() 404 .any(|value| value == "wss://relay.example.test") 405 ); 406 assert!(!address.iter().any(|value| value == " ")); 407 408 let mut tags = report_build_tags(&file_report()).unwrap(); 409 let sha = tags 410 .iter_mut() 411 .find(|tag| tag.first().map(String::as_str) == Some(TAG_SHA256)) 412 .expect("sha tag"); 413 sha.truncate(2); 414 let decoded = report_from_event(KIND_REPORT, &tags, "").unwrap(); 415 assert_eq!( 416 decoded.file.and_then(|file| file.sha256).as_deref(), 417 Some(FILE_HASH) 418 ); 419 420 let mut tags = report_build_tags(&file_report()).unwrap(); 421 replace_tag_value(&mut tags, TAG_SHA256, "bad"); 422 let err = report_from_event(KIND_REPORT, &tags, "").unwrap_err(); 423 assert!(matches!(err, EventParseError::InvalidTag(TAG_SHA256))); 424 } 425 426 #[test] 427 fn report_wrappers_preserve_event_metadata() { 428 let parts = to_wire_parts(&event_report()).unwrap(); 429 let data = data_from_event( 430 "report_id".to_string(), 431 "author".to_string(), 432 12, 433 parts.kind, 434 parts.content.clone(), 435 parts.tags.clone(), 436 ) 437 .unwrap(); 438 assert_eq!(data.kind, KIND_REPORT); 439 assert_eq!(data.data.report_type, RadrootsReportType::Illegal); 440 441 let parsed = parsed_from_event( 442 "report_id".to_string(), 443 "author".to_string(), 444 12, 445 parts.kind, 446 parts.content, 447 parts.tags, 448 "sig".to_string(), 449 ) 450 .unwrap(); 451 assert_eq!(parsed.event.sig, "sig"); 452 assert_eq!(parsed.data.data.reported_pubkey, REPORTED); 453 }