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 }