lib

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

coverage.rs (16063B)


      1 #[path = "../src/test_fixtures.rs"]
      2 mod test_fixtures;
      3 
      4 use std::borrow::Cow;
      5 
      6 use nostr::nips::nip04;
      7 use radroots_nostr::error::RadrootsNostrTagsResolveError;
      8 use radroots_nostr::events::jobs::{
      9     radroots_nostr_build_event_job_feedback, radroots_nostr_build_event_job_result,
     10 };
     11 use radroots_nostr::events::metadata::radroots_nostr_build_metadata_event;
     12 use radroots_nostr::events::post::{
     13     radroots_nostr_build_post_event, radroots_nostr_build_post_reply_event,
     14     radroots_nostr_post_events_filter,
     15 };
     16 use radroots_nostr::events::radroots_nostr_build_event;
     17 use radroots_nostr::filter::{
     18     radroots_nostr_filter_kind, radroots_nostr_filter_new_events, radroots_nostr_filter_tag,
     19     radroots_nostr_kind,
     20 };
     21 use radroots_nostr::parse::{radroots_nostr_parse_pubkey, radroots_nostr_parse_pubkeys};
     22 use radroots_nostr::tags::{
     23     radroots_nostr_tag_at_value, radroots_nostr_tag_first_value, radroots_nostr_tag_match_geohash,
     24     radroots_nostr_tag_match_l, radroots_nostr_tag_match_location,
     25     radroots_nostr_tag_match_summary, radroots_nostr_tag_match_title,
     26     radroots_nostr_tag_relays_parse, radroots_nostr_tag_slice, radroots_nostr_tags_match,
     27     radroots_nostr_tags_resolve,
     28 };
     29 use radroots_nostr::types::{
     30     RadrootsNostrEventBuilder, RadrootsNostrKeys, RadrootsNostrKind, RadrootsNostrMetadata,
     31     RadrootsNostrRelayUrl, RadrootsNostrTag, RadrootsNostrTagKind, RadrootsNostrTagStandard,
     32     RadrootsNostrTimestamp,
     33 };
     34 use radroots_nostr::util::{
     35     created_at_u32_saturating, event_created_at_u32_saturating, radroots_nostr_npub_string,
     36 };
     37 use test_fixtures::RELAY_PRIMARY_WSS;
     38 
     39 fn make_keys() -> RadrootsNostrKeys {
     40     RadrootsNostrKeys::generate()
     41 }
     42 
     43 fn text_event_with_tags(keys: &RadrootsNostrKeys, tags: Vec<RadrootsNostrTag>) -> nostr::Event {
     44     RadrootsNostrEventBuilder::new(RadrootsNostrKind::TextNote, "content")
     45         .tags(tags)
     46         .sign_with_keys(keys)
     47         .expect("sign event")
     48 }
     49 
     50 fn encrypted_event_with_p_tag(
     51     sender_keys: &RadrootsNostrKeys,
     52     content: impl Into<String>,
     53     recipient_hex: &str,
     54 ) -> nostr::Event {
     55     RadrootsNostrEventBuilder::new(RadrootsNostrKind::TextNote, content.into())
     56         .tags(vec![
     57             RadrootsNostrTag::custom(
     58                 RadrootsNostrTagKind::Encrypted,
     59                 vec!["encrypted".to_string()],
     60             ),
     61             RadrootsNostrTag::custom(RadrootsNostrTagKind::p(), vec![recipient_hex.to_string()]),
     62         ])
     63         .sign_with_keys(sender_keys)
     64         .expect("sign encrypted event")
     65 }
     66 
     67 #[test]
     68 fn build_event_skips_empty_tag_slices() {
     69     let keys = make_keys();
     70     let pubkey_hex = keys.public_key().to_hex();
     71     let builder = radroots_nostr_build_event(
     72         1,
     73         "test",
     74         vec![vec![], vec!["p".to_string(), pubkey_hex.clone()]],
     75     )
     76     .expect("builder");
     77     let event = builder.build(keys.public_key());
     78     let has_self_p_tag = event.tags.iter().any(|tag| {
     79         tag.kind() == RadrootsNostrTagKind::p() && tag.content() == Some(pubkey_hex.as_str())
     80     });
     81     assert!(has_self_p_tag);
     82 
     83     let builder_string = radroots_nostr_build_event(
     84         1,
     85         String::from("test"),
     86         vec![vec![], vec!["x".to_string(), "v".to_string()]],
     87     )
     88     .expect("builder string");
     89     let event_string = builder_string.build(keys.public_key());
     90     assert_eq!(event_string.tags.len(), 1);
     91 }
     92 
     93 #[test]
     94 fn job_event_builders_are_callable() {
     95     let keys = make_keys();
     96     let job_request = RadrootsNostrEventBuilder::new(RadrootsNostrKind::Custom(5001), "job")
     97         .sign_with_keys(&keys)
     98         .expect("job request");
     99     let non_job_request = RadrootsNostrEventBuilder::new(RadrootsNostrKind::TextNote, "job")
    100         .sign_with_keys(&keys)
    101         .expect("non-job request");
    102 
    103     let job_result = radroots_nostr_build_event_job_result(
    104         &job_request,
    105         "ok",
    106         1,
    107         Some("bolt11".to_string()),
    108         Some(Vec::new()),
    109     )
    110     .expect("job result builder");
    111     let _ = job_result.build(keys.public_key());
    112 
    113     let feedback_ok = radroots_nostr_build_event_job_feedback(
    114         &job_request,
    115         "success",
    116         Some("extra".to_string()),
    117         Some(Vec::new()),
    118     )
    119     .expect("job feedback builder");
    120     let _ = feedback_ok.build(keys.public_key());
    121 
    122     let feedback_invalid =
    123         radroots_nostr_build_event_job_feedback(&job_request, "invalid-status", None, None)
    124             .expect("job feedback fallback builder");
    125     let _ = feedback_invalid.build(keys.public_key());
    126 
    127     let invalid_job_result = radroots_nostr_build_event_job_result(
    128         &non_job_request,
    129         "ok",
    130         1,
    131         Some("bolt11".to_string()),
    132         Some(Vec::new()),
    133     );
    134     assert!(invalid_job_result.is_err());
    135 }
    136 
    137 #[test]
    138 fn metadata_builder_is_callable() {
    139     let keys = make_keys();
    140     let metadata = RadrootsNostrMetadata::default();
    141     let builder = radroots_nostr_build_metadata_event(&metadata);
    142     let _ = builder.build(keys.public_key());
    143 }
    144 
    145 #[test]
    146 fn post_helpers_cover_success_and_error_paths() {
    147     let keys = make_keys();
    148     let parent = text_event_with_tags(&keys, Vec::new());
    149     let parent_id_hex = parent.id.to_hex();
    150     let author_hex = parent.pubkey.to_hex();
    151     let root_id_hex = parent.id.to_hex();
    152 
    153     let post_builder = radroots_nostr_build_post_event("hello");
    154     let _ = post_builder.build(keys.public_key());
    155 
    156     let _ = radroots_nostr_post_events_filter(None, None);
    157     let _ = radroots_nostr_post_events_filter(Some(10), Some(1_700_000_000));
    158 
    159     let reply_ok = radroots_nostr_build_post_reply_event(
    160         &parent_id_hex,
    161         &author_hex,
    162         "reply",
    163         Some(root_id_hex.as_str()),
    164     )
    165     .expect("reply event builder");
    166     let _ = reply_ok.build(keys.public_key());
    167 
    168     let reply_invalid_root = radroots_nostr_build_post_reply_event(
    169         &parent_id_hex,
    170         &author_hex,
    171         "reply",
    172         Some("not-hex-root"),
    173     )
    174     .expect("reply builder with invalid optional root");
    175     let _ = reply_invalid_root.build(keys.public_key());
    176     let reply_empty_root =
    177         radroots_nostr_build_post_reply_event(&parent_id_hex, &author_hex, "reply", Some(""))
    178             .expect("reply builder with empty optional root");
    179     let _ = reply_empty_root.build(keys.public_key());
    180     let reply_none_root =
    181         radroots_nostr_build_post_reply_event(&parent_id_hex, &author_hex, "reply", None)
    182             .expect("reply builder without optional root");
    183     let _ = reply_none_root.build(keys.public_key());
    184 
    185     let invalid_parent = radroots_nostr_build_post_reply_event("bad", &author_hex, "reply", None);
    186     assert!(invalid_parent.is_err());
    187 
    188     let invalid_author =
    189         radroots_nostr_build_post_reply_event(&parent_id_hex, "bad", "reply", None);
    190     assert!(invalid_author.is_err());
    191 }
    192 
    193 #[test]
    194 fn filter_helpers_cover_all_paths() {
    195     let filter = radroots_nostr_filter_kind(1);
    196     let filtered = radroots_nostr_filter_tag(filter, "p", vec!["x".to_string()]);
    197     assert!(filtered.is_ok());
    198 
    199     let empty_tag =
    200         radroots_nostr_filter_tag(radroots_nostr_filter_kind(1), "", vec!["x".to_string()]);
    201     assert!(empty_tag.is_err());
    202 
    203     let multi_tag =
    204         radroots_nostr_filter_tag(radroots_nostr_filter_kind(1), "pp", vec!["x".to_string()]);
    205     assert!(multi_tag.is_err());
    206 
    207     let invalid_tag =
    208         radroots_nostr_filter_tag(radroots_nostr_filter_kind(1), "1", vec!["x".to_string()]);
    209     assert!(invalid_tag.is_err());
    210 
    211     let _ = radroots_nostr_kind(30000);
    212     let _ = radroots_nostr_filter_new_events(radroots_nostr_filter_kind(1));
    213 }
    214 
    215 #[test]
    216 fn parse_helpers_cover_success_and_failure() {
    217     let keys = make_keys();
    218     let pubkey_hex = keys.public_key().to_hex();
    219     let ok = radroots_nostr_parse_pubkey(pubkey_hex.as_str());
    220     assert!(ok.is_ok());
    221 
    222     let invalid = radroots_nostr_parse_pubkey("invalid");
    223     assert!(invalid.is_err());
    224 
    225     let parsed = radroots_nostr_parse_pubkeys(&[pubkey_hex.clone()]);
    226     assert!(parsed.is_ok());
    227 
    228     let parse_err = radroots_nostr_parse_pubkeys(&[pubkey_hex, "invalid".to_string()]);
    229     assert!(parse_err.is_err());
    230 }
    231 
    232 #[test]
    233 fn tag_helpers_cover_matchers_and_resolve_paths() {
    234     let keys = make_keys();
    235     let other = make_keys();
    236 
    237     let custom_tag = RadrootsNostrTag::custom(
    238         RadrootsNostrTagKind::Custom(Cow::Borrowed("x")),
    239         vec!["v1".to_string(), "v2".to_string()],
    240     );
    241     assert_eq!(
    242         radroots_nostr_tag_first_value(&custom_tag, "x"),
    243         Some("v1".to_string())
    244     );
    245     assert_eq!(radroots_nostr_tag_first_value(&custom_tag, "y"), None);
    246     assert_eq!(
    247         radroots_nostr_tag_at_value(&custom_tag, 0),
    248         Some("x".to_string())
    249     );
    250     assert_eq!(radroots_nostr_tag_at_value(&custom_tag, 9), None);
    251     assert_eq!(
    252         radroots_nostr_tag_slice(&custom_tag, 1),
    253         Some(vec!["v1".to_string(), "v2".to_string()])
    254     );
    255     assert_eq!(radroots_nostr_tag_slice(&custom_tag, 9), None);
    256     let matched = radroots_nostr_tags_match(&custom_tag).expect("custom match");
    257     assert_eq!(matched.0, "x");
    258     assert_eq!(matched.1, ["v1".to_string(), "v2".to_string()]);
    259 
    260     let relays_tag = RadrootsNostrTag::from_standardized(RadrootsNostrTagStandard::Relays(vec![
    261         RadrootsNostrRelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay"),
    262     ]));
    263     assert!(radroots_nostr_tag_relays_parse(&relays_tag).is_some());
    264     let relays_non_match =
    265         RadrootsNostrTag::from_standardized(RadrootsNostrTagStandard::Title("x".to_string()));
    266     assert!(radroots_nostr_tag_relays_parse(&relays_non_match).is_none());
    267     assert!(radroots_nostr_tag_relays_parse(&custom_tag).is_none());
    268 
    269     let l_tag = RadrootsNostrTag::custom(
    270         RadrootsNostrTagKind::Custom(Cow::Borrowed("l")),
    271         vec!["12.5".to_string(), "kg".to_string()],
    272     );
    273     assert_eq!(radroots_nostr_tag_match_l(&l_tag), Some(("kg", 12.5)));
    274     let bad_l_tag = RadrootsNostrTag::custom(
    275         RadrootsNostrTagKind::Custom(Cow::Borrowed("l")),
    276         vec!["abc".to_string(), "kg".to_string()],
    277     );
    278     assert_eq!(radroots_nostr_tag_match_l(&bad_l_tag), None);
    279     assert_eq!(radroots_nostr_tag_match_l(&custom_tag), None);
    280     let short_l_tag = RadrootsNostrTag::custom(
    281         RadrootsNostrTagKind::Custom(Cow::Borrowed("l")),
    282         vec!["12.5".to_string()],
    283     );
    284     assert_eq!(radroots_nostr_tag_match_l(&short_l_tag), None);
    285 
    286     let location_tag = RadrootsNostrTag::custom(
    287         RadrootsNostrTagKind::Custom(Cow::Borrowed("location")),
    288         vec![
    289             "se".to_string(),
    290             "stockholm".to_string(),
    291             "city".to_string(),
    292         ],
    293     );
    294     assert_eq!(
    295         radroots_nostr_tag_match_location(&location_tag),
    296         Some(("se", "stockholm", "city"))
    297     );
    298     let location_non_match = RadrootsNostrTag::custom(
    299         RadrootsNostrTagKind::Custom(Cow::Borrowed("x")),
    300         vec![
    301             "se".to_string(),
    302             "stockholm".to_string(),
    303             "city".to_string(),
    304         ],
    305     );
    306     assert_eq!(radroots_nostr_tag_match_location(&location_non_match), None);
    307     assert_eq!(radroots_nostr_tag_match_location(&custom_tag), None);
    308 
    309     let geohash_tag =
    310         RadrootsNostrTag::from_standardized(RadrootsNostrTagStandard::Geohash("u4pr".to_string()));
    311     assert_eq!(
    312         radroots_nostr_tag_match_geohash(&geohash_tag),
    313         Some("u4pr".to_string())
    314     );
    315     let title_tag =
    316         RadrootsNostrTag::from_standardized(RadrootsNostrTagStandard::Title("title".to_string()));
    317     assert_eq!(radroots_nostr_tag_match_geohash(&title_tag), None);
    318     assert_eq!(radroots_nostr_tag_match_geohash(&custom_tag), None);
    319 
    320     assert_eq!(
    321         radroots_nostr_tag_match_title(&title_tag),
    322         Some("title".to_string())
    323     );
    324     let summary_tag = RadrootsNostrTag::from_standardized(RadrootsNostrTagStandard::Summary(
    325         "summary".to_string(),
    326     ));
    327     assert_eq!(radroots_nostr_tag_match_title(&summary_tag), None);
    328     assert_eq!(radroots_nostr_tag_match_title(&custom_tag), None);
    329 
    330     assert_eq!(
    331         radroots_nostr_tag_match_summary(&summary_tag),
    332         Some("summary".to_string())
    333     );
    334     assert_eq!(radroots_nostr_tag_match_summary(&geohash_tag), None);
    335     assert_eq!(radroots_nostr_tag_match_summary(&custom_tag), None);
    336 
    337     let clear_event = text_event_with_tags(
    338         &keys,
    339         vec![RadrootsNostrTag::custom(
    340             RadrootsNostrTagKind::Custom(Cow::Borrowed("x")),
    341             vec!["x".to_string(), "v".to_string()],
    342         )],
    343     );
    344     let resolved = radroots_nostr_tags_resolve(&clear_event, &keys).expect("clear tags");
    345     assert_eq!(resolved.len(), 1);
    346 
    347     let encrypted_missing_p = text_event_with_tags(
    348         &keys,
    349         vec![RadrootsNostrTag::custom(
    350             RadrootsNostrTagKind::Encrypted,
    351             vec!["encrypted".to_string()],
    352         )],
    353     );
    354     let missing_p = radroots_nostr_tags_resolve(&encrypted_missing_p, &keys);
    355     assert!(matches!(
    356         missing_p,
    357         Err(RadrootsNostrTagsResolveError::MissingPTag(_))
    358     ));
    359 
    360     let sender = make_keys();
    361     let encrypted_invalid_p = encrypted_event_with_p_tag(&sender, "cipher", "not-a-pubkey");
    362     let invalid_p = radroots_nostr_tags_resolve(&encrypted_invalid_p, &keys);
    363     assert!(matches!(
    364         invalid_p,
    365         Err(RadrootsNostrTagsResolveError::MissingPTag(_))
    366     ));
    367 
    368     let encrypted_empty_p_content =
    369         RadrootsNostrEventBuilder::new(RadrootsNostrKind::TextNote, "cipher")
    370             .tags(vec![
    371                 RadrootsNostrTag::custom(
    372                     RadrootsNostrTagKind::Encrypted,
    373                     vec!["encrypted".to_string()],
    374                 ),
    375                 RadrootsNostrTag::custom(RadrootsNostrTagKind::p(), Vec::<String>::new()),
    376             ])
    377             .sign_with_keys(&sender)
    378             .expect("sign encrypted event with empty p tag");
    379     let empty_p_content = radroots_nostr_tags_resolve(&encrypted_empty_p_content, &keys);
    380     assert!(matches!(
    381         empty_p_content,
    382         Err(RadrootsNostrTagsResolveError::MissingPTag(_))
    383     ));
    384 
    385     let encrypted_not_recipient =
    386         encrypted_event_with_p_tag(&sender, "cipher", &other.public_key().to_hex());
    387     let not_recipient = radroots_nostr_tags_resolve(&encrypted_not_recipient, &keys);
    388     assert!(matches!(
    389         not_recipient,
    390         Err(RadrootsNostrTagsResolveError::NotRecipient)
    391     ));
    392 
    393     let encrypted_bad_cipher =
    394         encrypted_event_with_p_tag(&sender, "not-ciphertext", &keys.public_key().to_hex());
    395     let bad_cipher = radroots_nostr_tags_resolve(&encrypted_bad_cipher, &keys);
    396     assert!(matches!(
    397         bad_cipher,
    398         Err(RadrootsNostrTagsResolveError::DecryptionError(_))
    399     ));
    400 
    401     let encrypted_cleartext = nip04::encrypt(sender.secret_key(), &keys.public_key(), "[]")
    402         .expect("encrypt cleartext tags");
    403     let encrypted_ok =
    404         encrypted_event_with_p_tag(&sender, encrypted_cleartext, &keys.public_key().to_hex());
    405     let resolved_encrypted =
    406         radroots_nostr_tags_resolve(&encrypted_ok, &keys).expect("resolve tags");
    407     assert!(resolved_encrypted.is_empty());
    408 
    409     let encrypted_bad_json = nip04::encrypt(sender.secret_key(), &keys.public_key(), "not-json")
    410         .expect("encrypt invalid tags payload");
    411     let encrypted_bad_json_event =
    412         encrypted_event_with_p_tag(&sender, encrypted_bad_json, &keys.public_key().to_hex());
    413     let bad_json = radroots_nostr_tags_resolve(&encrypted_bad_json_event, &keys);
    414     assert!(matches!(
    415         bad_json,
    416         Err(RadrootsNostrTagsResolveError::ParseError(_))
    417     ));
    418 }
    419 
    420 #[test]
    421 fn util_helpers_cover_conversion_paths() {
    422     let keys = make_keys();
    423     let npub = radroots_nostr_npub_string(&keys.public_key());
    424     assert!(npub.is_some());
    425 
    426     let max = RadrootsNostrTimestamp::from(u64::from(u32::MAX));
    427     let overflow = RadrootsNostrTimestamp::from(u64::from(u32::MAX) + 1);
    428     assert_eq!(created_at_u32_saturating(max), u32::MAX);
    429     assert_eq!(created_at_u32_saturating(overflow), u32::MAX);
    430 
    431     let event = text_event_with_tags(&keys, Vec::new());
    432     let _ = event_created_at_u32_saturating(&event);
    433 }