nip10_reply_profile.rs (7279B)
1 mod support; 2 3 use nostr::{Keys as RadrootsNostrKeys, SecretKey as RadrootsNostrSecretKey}; 4 use radroots_event::post::reply::{AuthoredNip10Reply, Nip10ReplyReference}; 5 use radroots_event_codec::{ 6 admission::{ 7 post::{RadrootsPostAdmissionOutcome, verify_and_admit_post_event}, 8 reply::{admit_thread_excluded_post_candidate, verify_and_admit_nip10_reply_event}, 9 }, 10 decode::reply::RadrootsNip10ReplyStyle, 11 }; 12 use radroots_nostr::Error; 13 use radroots_nostr::event::GenericBuilder; 14 use radroots_nostr::event::Kind as RadrootsNostrKind; 15 use radroots_nostr::event::Timestamp as RadrootsNostrTimestamp; 16 use radroots_nostr::event::build_nip10_reply as build_nip10_reply_event; 17 use radroots_nostr::event::from_nostr; 18 use radroots_nostr::tag::Tag as RadrootsNostrTag; 19 use support::{ 20 FIXTURE_ALICE_SECRET_KEY_HEX, FIXTURE_BOB_PUBLIC_KEY_HEX, FIXTURE_CAROL_PUBLIC_KEY_HEX, 21 RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS, 22 }; 23 24 const CREATED_AT: u64 = 1_784_347_200; 25 const ROOT_EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 26 const PARENT_EVENT_ID: &str = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; 27 28 #[test] 29 fn typed_nip10_reply_builders_sign_exact_marked_direct_and_nested_profiles() { 30 let keys = fixture_keys(); 31 let created_at = RadrootsNostrTimestamp::from_secs(CREATED_AT); 32 let root = reference( 33 ROOT_EVENT_ID, 34 FIXTURE_BOB_PUBLIC_KEY_HEX, 35 Some(RELAY_PRIMARY_WSS), 36 ); 37 38 let direct = 39 AuthoredNip10Reply::direct("Direct reply", root.clone()).expect("authored direct reply"); 40 let direct_event = build_nip10_reply_event(&direct) 41 .expect("typed direct Reply builder") 42 .custom_created_at(created_at) 43 .sign_with_keys(&keys) 44 .expect("signed direct Reply"); 45 46 assert_eq!(direct_event.kind, RadrootsNostrKind::TextNote); 47 assert_eq!(direct_event.created_at, created_at); 48 assert_eq!( 49 event_tags(&direct_event), 50 vec![ 51 tag(&["e", ROOT_EVENT_ID, RELAY_PRIMARY_WSS, "root"]), 52 tag(&["p", FIXTURE_BOB_PUBLIC_KEY_HEX]), 53 ] 54 ); 55 direct_event.verify().expect("valid direct Reply signature"); 56 57 let parent = reference( 58 PARENT_EVENT_ID, 59 FIXTURE_CAROL_PUBLIC_KEY_HEX, 60 Some(RELAY_SECONDARY_WSS), 61 ); 62 let nested = 63 AuthoredNip10Reply::nested("Nested reply", root, parent).expect("authored nested reply"); 64 let nested_event = build_nip10_reply_event(&nested) 65 .expect("typed nested Reply builder") 66 .custom_created_at(created_at) 67 .sign_with_keys(&keys) 68 .expect("signed nested Reply"); 69 70 assert_eq!( 71 event_tags(&nested_event), 72 vec![ 73 tag(&["e", ROOT_EVENT_ID, RELAY_PRIMARY_WSS, "root"]), 74 tag(&["e", PARENT_EVENT_ID, RELAY_SECONDARY_WSS, "reply"]), 75 tag(&["p", FIXTURE_BOB_PUBLIC_KEY_HEX]), 76 tag(&["p", FIXTURE_CAROL_PUBLIC_KEY_HEX]), 77 ] 78 ); 79 nested_event.verify().expect("valid nested Reply signature"); 80 let admitted = verify_and_admit_nip10_reply_event( 81 from_nostr(&nested_event).expect("nested Reply adapter"), 82 ) 83 .expect("nested Reply admission"); 84 assert!(!admitted.projection().is_direct()); 85 assert_eq!( 86 admitted 87 .projection() 88 .reply_reference() 89 .expect("nested parent") 90 .event_id() 91 .to_hex(), 92 PARENT_EVENT_ID 93 ); 94 } 95 96 #[test] 97 fn signed_nip10_reply_is_thread_excluded_before_semantic_reply_admission() { 98 let reply = AuthoredNip10Reply::direct( 99 "Thread reply", 100 reference( 101 ROOT_EVENT_ID, 102 FIXTURE_BOB_PUBLIC_KEY_HEX, 103 Some(RELAY_PRIMARY_WSS), 104 ), 105 ) 106 .expect("authored direct reply"); 107 let event = build_nip10_reply_event(&reply) 108 .expect("typed Reply builder") 109 .custom_created_at(RadrootsNostrTimestamp::from_secs(CREATED_AT)) 110 .sign_with_keys(&fixture_keys()) 111 .expect("signed Reply"); 112 let envelope = from_nostr(&event).expect("Radroots event adapter"); 113 114 let candidate = match verify_and_admit_post_event(envelope).expect("post admission") { 115 RadrootsPostAdmissionOutcome::ThreadExcluded(candidate) => candidate, 116 RadrootsPostAdmissionOutcome::Root(_) => panic!("Reply must never become a root card"), 117 _ => panic!("unexpected post admission outcome"), 118 }; 119 let admitted = 120 admit_thread_excluded_post_candidate(candidate).expect("semantic Reply admission"); 121 122 assert_eq!(admitted.contract().id, "radroots.social.reply.v1"); 123 assert_eq!( 124 admitted.projection().style(), 125 RadrootsNip10ReplyStyle::Marked 126 ); 127 assert!(admitted.projection().is_direct()); 128 assert_eq!( 129 admitted.projection().root().event_id().to_hex(), 130 ROOT_EVENT_ID 131 ); 132 } 133 134 #[test] 135 fn verified_legacy_positional_nip10_reply_remains_tolerated_inbound() { 136 let keys = fixture_keys(); 137 let event = nostr::EventBuilder::text_note("Legacy positional reply") 138 .tags([ 139 RadrootsNostrTag::parse(["e", ROOT_EVENT_ID, RELAY_PRIMARY_WSS]) 140 .expect("positional root reference"), 141 RadrootsNostrTag::parse(["p", FIXTURE_BOB_PUBLIC_KEY_HEX]) 142 .expect("root author reference"), 143 ]) 144 .custom_created_at(RadrootsNostrTimestamp::from_secs(CREATED_AT)) 145 .sign_with_keys(&keys) 146 .expect("signed inbound fixture"); 147 let envelope = from_nostr(&event).expect("Radroots event adapter"); 148 let admitted = 149 verify_and_admit_nip10_reply_event(envelope).expect("legacy positional Reply admission"); 150 151 assert_eq!( 152 admitted.projection().style(), 153 RadrootsNip10ReplyStyle::LegacyPositional 154 ); 155 assert!(admitted.projection().is_direct()); 156 assert_eq!( 157 admitted.projection().root().event_id().to_hex(), 158 ROOT_EVENT_ID 159 ); 160 } 161 162 #[test] 163 fn generic_kind_one_builder_cannot_bypass_typed_nip10_reply_authoring() { 164 let keys = fixture_keys(); 165 let marked_reply = GenericBuilder::text_note("Raw marked reply").tags([ 166 RadrootsNostrTag::parse(["e", ROOT_EVENT_ID, RELAY_PRIMARY_WSS, "root"]) 167 .expect("root reference"), 168 RadrootsNostrTag::parse(["p", FIXTURE_BOB_PUBLIC_KEY_HEX]).expect("root author reference"), 169 ]); 170 171 for builder in [GenericBuilder::text_note("Raw root"), marked_reply] { 172 assert!(matches!( 173 builder.sign_with_keys(&keys), 174 Err(Error::TypedAuthoringRequired { kind }) 175 if kind == RadrootsNostrKind::TextNote.as_u16() 176 )); 177 } 178 } 179 180 fn fixture_keys() -> RadrootsNostrKeys { 181 RadrootsNostrKeys::new( 182 RadrootsNostrSecretKey::from_hex(FIXTURE_ALICE_SECRET_KEY_HEX).expect("fixture secret key"), 183 ) 184 } 185 186 fn reference(event_id: &str, author: &str, relay: Option<&str>) -> Nip10ReplyReference { 187 Nip10ReplyReference::parse(event_id, author, relay).expect("valid Reply reference") 188 } 189 190 fn event_tags(event: &nostr::Event) -> Vec<Vec<String>> { 191 event 192 .tags 193 .iter() 194 .map(|tag| tag.as_slice().to_vec()) 195 .collect() 196 } 197 198 fn tag(values: &[&str]) -> Vec<String> { 199 values.iter().map(|value| (*value).to_owned()).collect() 200 }