lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }