tests.rs (11766B)
1 use super::*; 2 use crate::contract::ContractMatchError; 3 use crate::envelope::EventEnvelopeParts; 4 use crate::envelope::kind::{ 5 KIND_FOLLOW, KIND_LIST_SET_GENERIC, KIND_POST, KIND_PROFILE, KIND_TRADE_PROPOSAL, 6 }; 7 use crate::id::parse_public_key; 8 9 fn hex_64(character: char) -> String { 10 crate::test_valid_hex_64(character) 11 } 12 13 fn hex_128(character: char) -> String { 14 core::iter::repeat_n(character, 128).collect() 15 } 16 17 fn event( 18 kind: u32, 19 id: &str, 20 author: &str, 21 created_at: u64, 22 tags: Vec<Vec<String>>, 23 ) -> EventEnvelope { 24 EventEnvelope::new(EventEnvelopeParts { 25 id: id.to_string(), 26 author: author.to_string(), 27 created_at, 28 kind, 29 tags, 30 content: String::new(), 31 sig: hex_128('f'), 32 }) 33 .expect("event envelope") 34 } 35 36 fn event_with_content( 37 kind: u32, 38 id: &str, 39 author: &str, 40 created_at: u64, 41 tags: Vec<Vec<String>>, 42 content: &str, 43 ) -> EventEnvelope { 44 EventEnvelope::new(EventEnvelopeParts { 45 id: id.to_string(), 46 author: author.to_string(), 47 created_at, 48 kind, 49 tags, 50 content: content.to_string(), 51 sig: hex_128('f'), 52 }) 53 .expect("event envelope") 54 } 55 56 fn candidate(id: char, created_at: u64) -> EventHeadCandidate { 57 expect_candidate(event_head_candidate_for_class( 58 &event(10002, &hex_64(id), &hex_64('a'), created_at, Vec::new()), 59 EventClass::Replaceable, 60 )) 61 } 62 63 fn expect_candidate(result: EventHeadCandidateResult) -> EventHeadCandidate { 64 match result { 65 EventHeadCandidateResult::Candidate(candidate) => candidate, 66 other => panic!("expected candidate: {other:?}"), 67 } 68 } 69 70 #[test] 71 fn regular_and_ephemeral_events_do_not_create_heads() { 72 let event = event(1, &hex_64('1'), &hex_64('a'), 1, Vec::new()); 73 assert_eq!( 74 event_head_candidate_for_class(&event, EventClass::Regular), 75 EventHeadCandidateResult::NotHeadSelected 76 ); 77 assert_eq!( 78 event_head_candidate_for_class(&event, EventClass::Ephemeral), 79 EventHeadCandidateResult::NotPersisted 80 ); 81 } 82 83 #[test] 84 fn replaceable_events_use_kind_and_pubkey_coordinates() { 85 let event = event(10002, &hex_64('1'), &hex_64('a'), 5, Vec::new()); 86 let candidate = expect_candidate(event_head_candidate_for_class( 87 &event, 88 EventClass::Replaceable, 89 )); 90 assert_eq!( 91 candidate.coordinate, 92 EventHeadCoordinate::Replaceable { 93 kind: 10002, 94 pubkey: parse_public_key(hex_64('a')).unwrap() 95 } 96 ); 97 assert_eq!(candidate.created_at, 5); 98 } 99 100 #[test] 101 fn addressable_events_use_kind_pubkey_and_d_tag_coordinates() { 102 let event = event( 103 30023, 104 &hex_64('2'), 105 &hex_64('b'), 106 7, 107 vec![vec![TAG_D.to_string(), "article-1".to_string()]], 108 ); 109 let candidate = expect_candidate(event_head_candidate_for_class( 110 &event, 111 EventClass::Addressable, 112 )); 113 assert_eq!( 114 candidate.coordinate, 115 EventHeadCoordinate::Addressable { 116 kind: 30023, 117 pubkey: parse_public_key(hex_64('b')).unwrap(), 118 d_tag: "article-1".to_owned() 119 } 120 ); 121 } 122 123 #[test] 124 fn addressable_events_require_valid_d_tags() { 125 let missing = event(30023, &hex_64('2'), &hex_64('b'), 7, Vec::new()); 126 assert_eq!( 127 event_head_candidate_for_class(&missing, EventClass::Addressable), 128 EventHeadCandidateResult::Malformed(EventHeadMalformed::MissingDTag) 129 ); 130 131 let invalid = event( 132 30023, 133 &hex_64('2'), 134 &hex_64('b'), 135 7, 136 vec![vec![TAG_D.to_string(), "bad d".to_string()]], 137 ); 138 assert!(matches!( 139 event_head_candidate_for_class(&invalid, EventClass::Addressable), 140 EventHeadCandidateResult::Malformed(EventHeadMalformed::InvalidDTag(_)) 141 )); 142 } 143 144 #[test] 145 fn event_head_selection_uses_nip01_time_and_lowest_id_rules() { 146 let current: CurrentEventHead = candidate('3', 10).into(); 147 148 assert!(matches!( 149 select_event_head(candidate('1', 1), None), 150 EventHeadDecision::Applied(_) 151 )); 152 assert!(matches!( 153 select_event_head(candidate('4', 11), Some(¤t)), 154 EventHeadDecision::Applied(_) 155 )); 156 assert_eq!( 157 select_event_head(candidate('2', 9), Some(¤t)), 158 EventHeadDecision::SkippedOlder 159 ); 160 assert_eq!( 161 select_event_head(candidate('3', 10), Some(¤t)), 162 EventHeadDecision::SkippedDuplicate 163 ); 164 assert!(matches!( 165 select_event_head(candidate('2', 10), Some(¤t)), 166 EventHeadDecision::Applied(_) 167 )); 168 assert_eq!( 169 select_event_head(candidate('4', 10), Some(¤t)), 170 EventHeadDecision::SkippedSameTimestampHigherEventId 171 ); 172 } 173 174 #[test] 175 fn event_head_selection_rejects_coordinate_mismatch() { 176 let current: CurrentEventHead = candidate('3', 10).into(); 177 let other = event_head_candidate_for_class( 178 &event( 179 30023, 180 &hex_64('2'), 181 &hex_64('a'), 182 11, 183 vec![vec![TAG_D.to_string(), "article".to_string()]], 184 ), 185 EventClass::Addressable, 186 ); 187 let other = expect_candidate(other); 188 assert_eq!( 189 select_event_head(other, Some(¤t)), 190 EventHeadDecision::CoordinateMismatch 191 ); 192 } 193 194 #[test] 195 fn contract_bridge_uses_replaceable_event_classes() { 196 let event = event(KIND_FOLLOW, &hex_64('1'), &hex_64('a'), 1, Vec::new()); 197 let candidate = expect_candidate(event_head_candidate_for_event(&event).expect("contract")); 198 assert_eq!( 199 candidate.coordinate, 200 EventHeadCoordinate::Replaceable { 201 kind: KIND_FOLLOW, 202 pubkey: parse_public_key(hex_64('a')).unwrap() 203 } 204 ); 205 } 206 207 #[test] 208 fn raw_nip01_bridge_uses_numeric_kind_classes_without_contract_identification() { 209 let replaceable = event(19_999, &hex_64('1'), &hex_64('a'), 1, Vec::new()); 210 let replaceable = expect_candidate(event_head_candidate_for_nip01_event(&replaceable)); 211 assert_eq!( 212 replaceable.coordinate, 213 EventHeadCoordinate::Replaceable { 214 kind: 19_999, 215 pubkey: parse_public_key(hex_64('a')).unwrap(), 216 } 217 ); 218 219 let addressable = event( 220 39_999, 221 &hex_64('2'), 222 &hex_64('b'), 223 2, 224 vec![vec![TAG_D.to_string(), "unsupported".to_string()]], 225 ); 226 let addressable = expect_candidate(event_head_candidate_for_nip01_event(&addressable)); 227 assert_eq!( 228 addressable.coordinate, 229 EventHeadCoordinate::Addressable { 230 kind: 39_999, 231 pubkey: parse_public_key(hex_64('b')).unwrap(), 232 d_tag: "unsupported".to_owned(), 233 } 234 ); 235 236 let regular = event(40_000, &hex_64('3'), &hex_64('c'), 3, Vec::new()); 237 assert_eq!( 238 event_head_candidate_for_nip01_event(®ular), 239 EventHeadCandidateResult::NotHeadSelected 240 ); 241 } 242 243 #[test] 244 fn raw_nip01_addressable_coordinates_treat_d_as_opaque_protocol_data() { 245 for (tags, expected) in [ 246 (Vec::new(), ""), 247 (vec![vec![TAG_D.to_owned(), String::new()]], ""), 248 ( 249 vec![ 250 vec![TAG_D.to_owned()], 251 vec![TAG_D.to_owned(), "ignored".to_owned()], 252 ], 253 "", 254 ), 255 ( 256 vec![vec![TAG_D.to_owned(), "not a product d".to_owned()]], 257 "not a product d", 258 ), 259 ( 260 vec![vec![TAG_D.to_owned(), "line\nbreak".to_owned()]], 261 "line\nbreak", 262 ), 263 ( 264 vec![ 265 vec![TAG_D.to_owned(), "first value".to_owned()], 266 vec![TAG_D.to_owned(), "second-value".to_owned()], 267 ], 268 "first value", 269 ), 270 ] { 271 let event = event(39_999, &hex_64('2'), &hex_64('b'), 2, tags); 272 let candidate = expect_candidate(event_head_candidate_for_nip01_event(&event)); 273 assert_eq!( 274 candidate.coordinate, 275 EventHeadCoordinate::Addressable { 276 kind: 39_999, 277 pubkey: parse_public_key(hex_64('b')).unwrap(), 278 d_tag: expected.to_owned(), 279 } 280 ); 281 } 282 } 283 284 #[test] 285 fn product_addressable_coordinates_retain_strict_d_validation() { 286 for tags in [ 287 Vec::new(), 288 vec![vec![TAG_D.to_owned(), String::new()]], 289 vec![vec![TAG_D.to_owned(), "not a product d".to_owned()]], 290 ] { 291 let event = event(30_023, &hex_64('2'), &hex_64('b'), 2, tags); 292 assert!(matches!( 293 event_head_candidate_for_class(&event, EventClass::Addressable), 294 EventHeadCandidateResult::Malformed(_) 295 )); 296 } 297 } 298 299 #[test] 300 fn contract_bridge_uses_addressable_event_classes() { 301 let event = event( 302 KIND_LIST_SET_GENERIC, 303 &hex_64('2'), 304 &hex_64('b'), 305 1, 306 vec![vec![TAG_D.to_string(), "member_of.farms".to_string()]], 307 ); 308 let candidate = expect_candidate(event_head_candidate_for_event(&event).expect("contract")); 309 assert_eq!( 310 candidate.coordinate, 311 EventHeadCoordinate::Addressable { 312 kind: KIND_LIST_SET_GENERIC, 313 pubkey: parse_public_key(hex_64('b')).unwrap(), 314 d_tag: "member_of.farms".to_owned() 315 } 316 ); 317 } 318 319 #[test] 320 fn contract_bridge_uses_profile_replaceable_heads() { 321 let profile = event_with_content( 322 KIND_PROFILE, 323 &hex_64('3'), 324 &hex_64('c'), 325 1, 326 Vec::new(), 327 r#"{"name":"Alice"}"#, 328 ); 329 let candidate = 330 expect_candidate(event_head_candidate_for_event(&profile).expect("profile contract")); 331 assert_eq!( 332 candidate.coordinate, 333 EventHeadCoordinate::Replaceable { 334 kind: KIND_PROFILE, 335 pubkey: parse_public_key(hex_64('c')).unwrap() 336 } 337 ); 338 } 339 340 #[test] 341 fn contract_bridge_keeps_trade_mutations_out_of_head_selection() { 342 let trade = event_with_content( 343 KIND_TRADE_PROPOSAL, 344 &hex_64('4'), 345 &hex_64('d'), 346 1, 347 vec![ 348 vec![ 349 "contract".to_string(), 350 "radroots.trade.proposal.v1".to_string(), 351 ], 352 vec!["p".to_string(), hex_64('e')], 353 vec![ 354 TAG_D.to_string(), 355 "11111111111111111111111111111111".to_string(), 356 ], 357 ], 358 r#"{"contract_id":"radroots.trade.proposal.v1"}"#, 359 ); 360 assert_eq!( 361 event_head_candidate_for_event(&trade).expect("trade contract"), 362 EventHeadCandidateResult::NotHeadSelected 363 ); 364 } 365 366 #[test] 367 fn contract_bridge_reports_unsupported_and_malformed_shapes() { 368 let unsupported = event(999_999, &hex_64('5'), &hex_64('a'), 1, Vec::new()); 369 assert_eq!( 370 event_head_candidate_for_event(&unsupported), 371 Err(ContractMatchError::UnsupportedKind(999_999)) 372 ); 373 374 let malformed_addressable = event( 375 KIND_LIST_SET_GENERIC, 376 &hex_64('6'), 377 &hex_64('a'), 378 1, 379 Vec::new(), 380 ); 381 assert_eq!( 382 event_head_candidate_for_event(&malformed_addressable), 383 Err(ContractMatchError::UnsupportedShape(KIND_LIST_SET_GENERIC)) 384 ); 385 386 let regular_with_d_tag = event( 387 KIND_POST, 388 &hex_64('7'), 389 &hex_64('a'), 390 1, 391 vec![vec![TAG_D.to_string(), "not-a-head".to_string()]], 392 ); 393 assert_eq!( 394 event_head_candidate_for_event(®ular_with_d_tag).expect("post contract"), 395 EventHeadCandidateResult::NotHeadSelected 396 ); 397 } 398 399 #[test] 400 fn expect_candidate_reports_non_candidate_inputs() { 401 let result = std::panic::catch_unwind(|| { 402 expect_candidate(EventHeadCandidateResult::NotHeadSelected); 403 }); 404 405 assert!(result.is_err()); 406 }