lib

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

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 }