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