lib

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

coverage.rs (17731B)


      1 #[path = "../src/test_fixtures.rs"]
      2 mod test_fixtures;
      3 
      4 use std::borrow::Cow;
      5 
      6 use nostr::{Keys as RadrootsNostrKeys, RelayUrl as RadrootsNostrRelayUrl, nips::nip04};
      7 #[cfg(feature = "events")]
      8 use radroots_event::post::reply::{AuthoredNip10Reply, Nip10ReplyReference};
      9 #[cfg(feature = "events")]
     10 use radroots_event_codec::decode::job::{JobEventBorrow, JobEventLike};
     11 #[cfg(feature = "events")]
     12 use radroots_nostr::event::build_nip10_reply as build_nip10_reply_event;
     13 #[cfg(feature = "events")]
     14 use radroots_nostr::event::{
     15     ApplicationHandlerSpec, EventAdapter, build_application_handler, metadata_has_fields,
     16     to_job_feedback_index, to_job_feedback_metadata, to_job_request_index, to_job_request_metadata,
     17     to_job_result_index, to_job_result_metadata,
     18 };
     19 use radroots_nostr::event::{Kind as RadrootsNostrKind, Timestamp as RadrootsNostrTimestamp};
     20 use radroots_nostr::event::{
     21     build_job_feedback as build_event_job_feedback, build_job_result as build_event_job_result,
     22     created_at_u32_saturating, event_created_at_u32_saturating, post_filter as post_events_filter,
     23 };
     24 use radroots_nostr::filter::{
     25     for_kind as filter_kind, kind, since_now as filter_new_events, with_tag as filter_tag,
     26 };
     27 use radroots_nostr::key::{parse_public_key, public_key_from_nostr, public_key_to_npub};
     28 use radroots_nostr::tag::{
     29     ResolveError, Tag as RadrootsNostrTag, TagKind as RadrootsNostrTagKind,
     30     TagStandard as RadrootsNostrTagStandard, first_value as tag_first_value,
     31     match_geohash as tag_match_geohash, match_location as tag_match_location,
     32     match_location_coordinate as tag_match_l, match_parts as tags_match,
     33     match_summary as tag_match_summary, match_title as tag_match_title,
     34     relay_urls as tag_relays_parse, resolve as tags_resolve, value_at as tag_at_value,
     35     values_from as tag_slice,
     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     nostr::EventBuilder::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     nostr::EventBuilder::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 job_event_builders_are_callable() {
     69     let keys = make_keys();
     70     let job_request = nostr::EventBuilder::new(RadrootsNostrKind::Custom(5001), "job")
     71         .sign_with_keys(&keys)
     72         .expect("job request");
     73     let non_job_request = nostr::EventBuilder::new(RadrootsNostrKind::TextNote, "job")
     74         .sign_with_keys(&keys)
     75         .expect("non-job request");
     76 
     77     let job_result = build_event_job_result(
     78         &job_request,
     79         "ok",
     80         1,
     81         Some("bolt11".to_string()),
     82         Some(Vec::new()),
     83     )
     84     .expect("job result builder");
     85     let _ = job_result
     86         .sign_with_keys(&keys)
     87         .expect("job result signs through the generic boundary");
     88 
     89     let feedback_ok = build_event_job_feedback(
     90         &job_request,
     91         "success",
     92         Some("extra".to_string()),
     93         Some(Vec::new()),
     94     )
     95     .expect("job feedback builder");
     96     let _ = feedback_ok
     97         .sign_with_keys(&keys)
     98         .expect("job feedback signs through the generic boundary");
     99 
    100     let feedback_invalid = build_event_job_feedback(&job_request, "invalid-status", None, None)
    101         .expect("job feedback fallback builder");
    102     let _ = feedback_invalid
    103         .sign_with_keys(&keys)
    104         .expect("fallback job feedback signs through the generic boundary");
    105 
    106     let invalid_job_result = build_event_job_result(
    107         &non_job_request,
    108         "ok",
    109         1,
    110         Some("bolt11".to_string()),
    111         Some(Vec::new()),
    112     );
    113     assert!(invalid_job_result.is_err());
    114 }
    115 
    116 #[test]
    117 fn post_helpers_cover_success_and_error_paths() {
    118     let _ = post_events_filter(None, None);
    119     let _ = post_events_filter(Some(10), Some(1_700_000_000));
    120 
    121     #[cfg(feature = "events")]
    122     {
    123         let keys = make_keys();
    124         let root = nostr::EventBuilder::text_note("root")
    125             .sign_with_keys(&keys)
    126             .expect("root");
    127         let parent = nostr::EventBuilder::text_note("parent")
    128             .sign_with_keys(&keys)
    129             .expect("parent");
    130         let author_hex = root.pubkey.to_hex();
    131 
    132         let root_reference =
    133             Nip10ReplyReference::parse(root.id.to_hex(), &author_hex, Some(RELAY_PRIMARY_WSS))
    134                 .expect("root reference");
    135         let direct = AuthoredNip10Reply::direct("direct reply", root_reference.clone())
    136             .expect("direct reply");
    137         let direct_builder = build_nip10_reply_event(&direct).expect("direct reply builder");
    138         let _ = direct_builder
    139             .sign_with_keys(&keys)
    140             .expect("direct reply signs through the typed boundary");
    141 
    142         let parent_reference = Nip10ReplyReference::parse(parent.id.to_hex(), &author_hex, None)
    143             .expect("parent reference");
    144         let nested = AuthoredNip10Reply::nested("nested reply", root_reference, parent_reference)
    145             .expect("nested reply");
    146         let nested_builder = build_nip10_reply_event(&nested).expect("nested reply builder");
    147         let _ = nested_builder
    148             .sign_with_keys(&keys)
    149             .expect("nested reply signs through the typed boundary");
    150 
    151         assert!(Nip10ReplyReference::parse("bad", &author_hex, None).is_err());
    152         assert!(Nip10ReplyReference::parse(root.id.to_hex(), "bad", None).is_err());
    153         assert!(
    154             Nip10ReplyReference::parse(
    155                 root.id.to_hex(),
    156                 &author_hex,
    157                 Some("https://relay.example"),
    158             )
    159             .is_err()
    160         );
    161         assert!(AuthoredNip10Reply::direct(" ", nested.root().clone()).is_err());
    162     }
    163 }
    164 
    165 #[test]
    166 fn filter_helpers_cover_all_paths() {
    167     let filter = filter_kind(1);
    168     let filtered = filter_tag(filter, "p", vec!["x".to_string()]);
    169     assert!(filtered.is_ok());
    170 
    171     let empty_tag = filter_tag(filter_kind(1), "", vec!["x".to_string()]);
    172     assert!(empty_tag.is_err());
    173 
    174     let multi_tag = filter_tag(filter_kind(1), "pp", vec!["x".to_string()]);
    175     assert!(multi_tag.is_err());
    176 
    177     let invalid_tag = filter_tag(filter_kind(1), "1", vec!["x".to_string()]);
    178     assert!(invalid_tag.is_err());
    179 
    180     let _ = kind(30000);
    181     let _ = filter_new_events(filter_kind(1));
    182 }
    183 
    184 #[test]
    185 fn parse_helpers_cover_success_and_failure() {
    186     let keys = make_keys();
    187     let pubkey_hex = keys.public_key().to_hex();
    188     let ok = parse_public_key(pubkey_hex.as_str());
    189     assert!(ok.is_ok());
    190 
    191     let invalid = parse_public_key("invalid");
    192     assert!(invalid.is_err());
    193 
    194     let npub = public_key_to_npub(ok.expect("public key")).expect("npub");
    195     assert!(parse_public_key(&npub).is_ok());
    196 }
    197 
    198 #[test]
    199 fn tag_helpers_cover_matchers_and_resolve_paths() {
    200     let keys = make_keys();
    201     let other = make_keys();
    202 
    203     let custom_tag = RadrootsNostrTag::custom(
    204         RadrootsNostrTagKind::Custom(Cow::Borrowed("x")),
    205         vec!["v1".to_string(), "v2".to_string()],
    206     );
    207     assert_eq!(tag_first_value(&custom_tag, "x"), Some("v1".to_string()));
    208     assert_eq!(tag_first_value(&custom_tag, "y"), None);
    209     assert_eq!(tag_at_value(&custom_tag, 0), Some("x".to_string()));
    210     assert_eq!(tag_at_value(&custom_tag, 9), None);
    211     assert_eq!(
    212         tag_slice(&custom_tag, 1),
    213         Some(vec!["v1".to_string(), "v2".to_string()])
    214     );
    215     assert_eq!(tag_slice(&custom_tag, 9), None);
    216     let matched = tags_match(&custom_tag).expect("custom match");
    217     assert_eq!(matched.0, "x");
    218     assert_eq!(matched.1, ["v1".to_string(), "v2".to_string()]);
    219 
    220     let relays_tag = RadrootsNostrTag::from_standardized(RadrootsNostrTagStandard::Relays(vec![
    221         RadrootsNostrRelayUrl::parse(RELAY_PRIMARY_WSS).expect("relay"),
    222     ]));
    223     assert!(tag_relays_parse(&relays_tag).is_some());
    224     let relays_non_match =
    225         RadrootsNostrTag::from_standardized(RadrootsNostrTagStandard::Title("x".to_string()));
    226     assert!(tag_relays_parse(&relays_non_match).is_none());
    227     assert!(tag_relays_parse(&custom_tag).is_none());
    228 
    229     let l_tag = RadrootsNostrTag::custom(
    230         RadrootsNostrTagKind::Custom(Cow::Borrowed("l")),
    231         vec!["12.5".to_string(), "kg".to_string()],
    232     );
    233     assert_eq!(tag_match_l(&l_tag), Some(("kg", 12.5)));
    234     let bad_l_tag = RadrootsNostrTag::custom(
    235         RadrootsNostrTagKind::Custom(Cow::Borrowed("l")),
    236         vec!["abc".to_string(), "kg".to_string()],
    237     );
    238     assert_eq!(tag_match_l(&bad_l_tag), None);
    239     assert_eq!(tag_match_l(&custom_tag), None);
    240     let short_l_tag = RadrootsNostrTag::custom(
    241         RadrootsNostrTagKind::Custom(Cow::Borrowed("l")),
    242         vec!["12.5".to_string()],
    243     );
    244     assert_eq!(tag_match_l(&short_l_tag), None);
    245 
    246     let location_tag = RadrootsNostrTag::custom(
    247         RadrootsNostrTagKind::Custom(Cow::Borrowed("location")),
    248         vec![
    249             "se".to_string(),
    250             "stockholm".to_string(),
    251             "city".to_string(),
    252         ],
    253     );
    254     assert_eq!(
    255         tag_match_location(&location_tag),
    256         Some(("se", "stockholm", "city"))
    257     );
    258     let location_non_match = RadrootsNostrTag::custom(
    259         RadrootsNostrTagKind::Custom(Cow::Borrowed("x")),
    260         vec![
    261             "se".to_string(),
    262             "stockholm".to_string(),
    263             "city".to_string(),
    264         ],
    265     );
    266     assert_eq!(tag_match_location(&location_non_match), None);
    267     assert_eq!(tag_match_location(&custom_tag), None);
    268 
    269     let geohash_tag =
    270         RadrootsNostrTag::from_standardized(RadrootsNostrTagStandard::Geohash("u4pr".to_string()));
    271     assert_eq!(tag_match_geohash(&geohash_tag), Some("u4pr".to_string()));
    272     let title_tag =
    273         RadrootsNostrTag::from_standardized(RadrootsNostrTagStandard::Title("title".to_string()));
    274     assert_eq!(tag_match_geohash(&title_tag), None);
    275     assert_eq!(tag_match_geohash(&custom_tag), None);
    276 
    277     assert_eq!(tag_match_title(&title_tag), Some("title".to_string()));
    278     let summary_tag = RadrootsNostrTag::from_standardized(RadrootsNostrTagStandard::Summary(
    279         "summary".to_string(),
    280     ));
    281     assert_eq!(tag_match_title(&summary_tag), None);
    282     assert_eq!(tag_match_title(&custom_tag), None);
    283 
    284     assert_eq!(tag_match_summary(&summary_tag), Some("summary".to_string()));
    285     assert_eq!(tag_match_summary(&geohash_tag), None);
    286     assert_eq!(tag_match_summary(&custom_tag), None);
    287 
    288     let clear_event = text_event_with_tags(
    289         &keys,
    290         vec![RadrootsNostrTag::custom(
    291             RadrootsNostrTagKind::Custom(Cow::Borrowed("x")),
    292             vec!["x".to_string(), "v".to_string()],
    293         )],
    294     );
    295     let resolved = tags_resolve(&clear_event, &keys).expect("clear tags");
    296     assert_eq!(resolved.len(), 1);
    297 
    298     let encrypted_missing_p = text_event_with_tags(
    299         &keys,
    300         vec![RadrootsNostrTag::custom(
    301             RadrootsNostrTagKind::Encrypted,
    302             vec!["encrypted".to_string()],
    303         )],
    304     );
    305     let missing_p = tags_resolve(&encrypted_missing_p, &keys);
    306     assert!(matches!(missing_p, Err(ResolveError::MissingPTag(_))));
    307 
    308     let sender = make_keys();
    309     let encrypted_invalid_p = encrypted_event_with_p_tag(&sender, "cipher", "not-a-pubkey");
    310     let invalid_p = tags_resolve(&encrypted_invalid_p, &keys);
    311     assert!(matches!(invalid_p, Err(ResolveError::MissingPTag(_))));
    312 
    313     let encrypted_empty_p_content = nostr::EventBuilder::new(RadrootsNostrKind::TextNote, "cipher")
    314         .tags(vec![
    315             RadrootsNostrTag::custom(
    316                 RadrootsNostrTagKind::Encrypted,
    317                 vec!["encrypted".to_string()],
    318             ),
    319             RadrootsNostrTag::custom(RadrootsNostrTagKind::p(), Vec::<String>::new()),
    320         ])
    321         .sign_with_keys(&sender)
    322         .expect("sign encrypted event with empty p tag");
    323     let empty_p_content = tags_resolve(&encrypted_empty_p_content, &keys);
    324     assert!(matches!(empty_p_content, Err(ResolveError::MissingPTag(_))));
    325 
    326     let encrypted_not_recipient =
    327         encrypted_event_with_p_tag(&sender, "cipher", &other.public_key().to_hex());
    328     let not_recipient = tags_resolve(&encrypted_not_recipient, &keys);
    329     assert!(matches!(not_recipient, Err(ResolveError::NotRecipient)));
    330 
    331     let encrypted_bad_cipher =
    332         encrypted_event_with_p_tag(&sender, "not-ciphertext", &keys.public_key().to_hex());
    333     let bad_cipher = tags_resolve(&encrypted_bad_cipher, &keys);
    334     assert!(matches!(bad_cipher, Err(ResolveError::DecryptionError(_))));
    335 
    336     let encrypted_cleartext = nip04::encrypt(sender.secret_key(), &keys.public_key(), "[]")
    337         .expect("encrypt cleartext tags");
    338     let encrypted_ok =
    339         encrypted_event_with_p_tag(&sender, encrypted_cleartext, &keys.public_key().to_hex());
    340     let resolved_encrypted = tags_resolve(&encrypted_ok, &keys).expect("resolve tags");
    341     assert!(resolved_encrypted.is_empty());
    342 
    343     let encrypted_bad_json = nip04::encrypt(sender.secret_key(), &keys.public_key(), "not-json")
    344         .expect("encrypt invalid tags payload");
    345     let encrypted_bad_json_event =
    346         encrypted_event_with_p_tag(&sender, encrypted_bad_json, &keys.public_key().to_hex());
    347     let bad_json = tags_resolve(&encrypted_bad_json_event, &keys);
    348     assert!(matches!(bad_json, Err(ResolveError::ParseError(_))));
    349 }
    350 
    351 #[test]
    352 fn util_helpers_cover_conversion_paths() {
    353     let keys = make_keys();
    354     let native = public_key_from_nostr(keys.public_key()).expect("native public key");
    355     let npub = public_key_to_npub(native).expect("npub");
    356     assert!(npub.starts_with("npub1"));
    357 
    358     let max = RadrootsNostrTimestamp::from(u64::from(u32::MAX));
    359     let overflow = RadrootsNostrTimestamp::from(u64::from(u32::MAX) + 1);
    360     assert_eq!(created_at_u32_saturating(max), u32::MAX);
    361     assert_eq!(created_at_u32_saturating(overflow), u32::MAX);
    362 
    363     let event = text_event_with_tags(&keys, Vec::new());
    364     let _ = event_created_at_u32_saturating(&event);
    365 }
    366 
    367 #[cfg(feature = "events")]
    368 #[test]
    369 fn event_and_job_adapters_cover_native_value_boundaries() {
    370     let keys = make_keys();
    371     let event = nostr::EventBuilder::new(RadrootsNostrKind::Custom(5_001), "job")
    372         .tags(vec![RadrootsNostrTag::custom(
    373             RadrootsNostrTagKind::Custom(Cow::Borrowed("i")),
    374             vec!["input".to_string()],
    375         )])
    376         .sign_with_keys(&keys)
    377         .unwrap();
    378 
    379     let adapter = EventAdapter::new(&event);
    380     assert_eq!(JobEventLike::raw_id(&adapter), event.id.to_hex());
    381     assert_eq!(JobEventLike::raw_author(&adapter), event.pubkey.to_hex());
    382     assert_eq!(
    383         JobEventLike::raw_published_at(&adapter),
    384         event.created_at.as_secs()
    385     );
    386     assert_eq!(JobEventLike::raw_kind(&adapter), 5_001);
    387     assert_eq!(JobEventLike::raw_content(&adapter), "job");
    388     assert_eq!(JobEventLike::raw_tags(&adapter).len(), 1);
    389     assert_eq!(JobEventLike::raw_sig(&adapter), event.sig.to_string());
    390     assert_eq!(JobEventBorrow::raw_id(&adapter), event.id.to_hex());
    391     assert_eq!(JobEventBorrow::raw_author(&adapter), event.pubkey.to_hex());
    392     assert_eq!(JobEventBorrow::raw_content(&adapter), "job");
    393     assert_eq!(JobEventBorrow::raw_kind(&adapter), 5_001);
    394 
    395     let profile_event = nostr::EventBuilder::metadata(&nostr::Metadata::new().name("Alice"))
    396         .sign_with_keys(&keys)
    397         .unwrap();
    398     let ordinary_adapter = EventAdapter::new(&profile_event);
    399     assert_eq!(JobEventLike::raw_kind(&ordinary_adapter), 0);
    400     assert_eq!(JobEventBorrow::raw_kind(&ordinary_adapter), 0);
    401     let _ = to_job_request_metadata(&event);
    402     let _ = to_job_request_index(&event);
    403     let _ = to_job_result_metadata(&event);
    404     let _ = to_job_result_index(&event);
    405     let _ = to_job_feedback_metadata(&event);
    406     let _ = to_job_feedback_index(&event);
    407 }
    408 
    409 #[cfg(feature = "events")]
    410 #[test]
    411 fn application_handler_builder_covers_optional_metadata_and_tag_filters() {
    412     assert!(build_application_handler(&ApplicationHandlerSpec::new(Vec::new())).is_err());
    413     let empty = nostr::Metadata::new();
    414     assert!(!metadata_has_fields(&empty));
    415     assert!(
    416         build_application_handler(
    417             &ApplicationHandlerSpec::new(vec![1]).with_metadata(empty.clone())
    418         )
    419         .is_ok()
    420     );
    421     assert!(build_application_handler(&ApplicationHandlerSpec::new(vec![1])).is_ok());
    422     let metadata = nostr::Metadata::new().name("Market app");
    423     assert!(metadata_has_fields(&metadata));
    424     let spec = ApplicationHandlerSpec::new(vec![1, 30_001])
    425         .with_identifier("market-app")
    426         .with_metadata(metadata)
    427         .with_relays(vec![" ".into(), RELAY_PRIMARY_WSS.into()])
    428         .with_nostr_connect_url(" nostrconnect://app ")
    429         .with_extra_tags(vec![Vec::new(), vec!["x".into(), "value".into()]]);
    430     assert_eq!(spec.kinds(), [1, 30_001]);
    431     assert_eq!(spec.identifier(), Some("market-app"));
    432     assert!(spec.metadata().is_some());
    433     assert_eq!(spec.extra_tags().len(), 2);
    434     assert_eq!(spec.relays().len(), 2);
    435     assert_eq!(spec.nostr_connect_url(), Some(" nostrconnect://app "));
    436     let builder = build_application_handler(&spec).unwrap();
    437     let event = builder.sign_with_keys(&make_keys()).unwrap();
    438     assert_eq!(event.kind, RadrootsNostrKind::Custom(31_990));
    439 }