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 }