nip17_conformance.rs (10950B)
1 #![cfg(feature = "nip17")] 2 3 mod support; 4 5 use nostr::{Keys, SecretKey}; 6 use radroots_event::social::message::{Message, MessageRecipient}; 7 use radroots_event::social::message_file::{MessageFile, MessageFileDimensions}; 8 use radroots_nostr::nip17::{ 9 Rumor, WrapOptions, unwrap_gift_wrap, wrap_message, wrap_message_file, 10 }; 11 use serde::Deserialize; 12 use serde_json::Value; 13 use std::{borrow::Cow, fs, path::Path}; 14 use support::{ 15 FIXTURE_ALICE_PUBLIC_KEY_HEX, FIXTURE_ALICE_SECRET_KEY_HEX, FIXTURE_BOB_PUBLIC_KEY_HEX, 16 FIXTURE_BOB_SECRET_KEY_HEX, 17 }; 18 19 const PACKAGED_VECTORS: &str = include_str!("fixtures/nip17_adapter.v1.json"); 20 const WORKSPACE_VECTOR_PATH: &str = "../../contracts/conformance/vectors/nip17/adapter.v1.json"; 21 const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml"; 22 23 #[derive(Debug, Deserialize)] 24 struct Suite { 25 suite: String, 26 contract_version: String, 27 vectors: Vec<Vector>, 28 } 29 30 #[derive(Debug, Deserialize)] 31 struct Vector { 32 id: String, 33 kind: String, 34 input: Value, 35 expected: Value, 36 } 37 38 #[tokio::test] 39 async fn checked_in_nip17_vectors_execute_against_public_api() { 40 let vectors = conformance_vectors(); 41 let suite: Suite = serde_json::from_str(&vectors).expect("NIP-17 vectors must parse"); 42 assert_eq!(suite.suite, "nip17_adapter"); 43 assert_eq!(suite.contract_version, "1.0.0"); 44 assert!(!suite.vectors.is_empty()); 45 46 for vector in &suite.vectors { 47 execute_vector(vector).await; 48 } 49 } 50 51 fn conformance_vectors() -> Cow<'static, str> { 52 let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(WORKSPACE_VECTOR_PATH); 53 match fs::read_to_string(&workspace_path) { 54 Ok(canonical) => { 55 assert_eq!( 56 canonical, 57 PACKAGED_VECTORS, 58 "packaged NIP-17 vectors must match {}", 59 workspace_path.display() 60 ); 61 Cow::Owned(canonical) 62 } 63 Err(error) 64 if error.kind() == std::io::ErrorKind::NotFound 65 && !Path::new(env!("CARGO_MANIFEST_DIR")) 66 .join(WORKSPACE_CONTRACT_MARKER_PATH) 67 .is_file() => 68 { 69 Cow::Borrowed(PACKAGED_VECTORS) 70 } 71 Err(error) => panic!("failed to read {}: {error}", workspace_path.display()), 72 } 73 } 74 75 async fn execute_vector(vector: &Vector) { 76 match vector.kind.as_str() { 77 "nip17.message.roundtrip" => execute_message_roundtrip(vector).await, 78 "nip17.message_file.roundtrip" => execute_message_file_roundtrip(vector).await, 79 "nip17.message.invalid_recipient" => execute_invalid_recipient(vector).await, 80 kind => panic!("{} uses unsupported vector kind {kind}", vector.id), 81 } 82 } 83 84 async fn execute_message_roundtrip(vector: &Vector) { 85 assert_eq!( 86 input_str(vector, "recipient_public_key"), 87 FIXTURE_BOB_PUBLIC_KEY_HEX 88 ); 89 let sender = sender_keys(); 90 let receiver = receiver_keys(); 91 let message = Message { 92 recipients: vec![MessageRecipient { 93 public_key: input_str(vector, "recipient_public_key").to_owned(), 94 relay_url: None, 95 }], 96 content: input_str(vector, "content").to_owned(), 97 reply_to: None, 98 subject: Some(input_str(vector, "subject").to_owned()), 99 }; 100 let events = wrap_message(&sender, &message, options(vector)) 101 .await 102 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 103 assert_eq!(events.len(), expected_u64(vector, "event_count") as usize); 104 105 let rumor = unwrap_gift_wrap(&receiver, &events[0]) 106 .await 107 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 108 let rendered = format!("{rumor:?}"); 109 for private_value in [ 110 message.content.as_str(), 111 FIXTURE_ALICE_PUBLIC_KEY_HEX, 112 FIXTURE_BOB_PUBLIC_KEY_HEX, 113 ] { 114 assert!(!rendered.contains(private_value), "{}", vector.id); 115 } 116 match rumor { 117 Rumor::Message(metadata) => { 118 assert_eq!( 119 metadata.author, 120 expected_str(vector, "author"), 121 "{}", 122 vector.id 123 ); 124 assert_eq!( 125 metadata.published_at, 126 expected_u64(vector, "published_at"), 127 "{}", 128 vector.id 129 ); 130 assert_eq!( 131 metadata.data.content, 132 expected_str(vector, "content"), 133 "{}", 134 vector.id 135 ); 136 assert_eq!( 137 metadata.data.recipients.len(), 138 expected_u64(vector, "recipient_count") as usize, 139 "{}", 140 vector.id 141 ); 142 assert_eq!( 143 metadata.data.subject.as_deref(), 144 Some(expected_str(vector, "subject")), 145 "{}", 146 vector.id 147 ); 148 } 149 other => panic!("{} expected a message rumor, got {other:?}", vector.id), 150 } 151 } 152 153 async fn execute_message_file_roundtrip(vector: &Vector) { 154 assert_eq!( 155 input_str(vector, "recipient_public_key"), 156 FIXTURE_BOB_PUBLIC_KEY_HEX 157 ); 158 let sender = sender_keys(); 159 let receiver = receiver_keys(); 160 let message = MessageFile { 161 recipients: vec![MessageRecipient { 162 public_key: input_str(vector, "recipient_public_key").to_owned(), 163 relay_url: None, 164 }], 165 file_url: input_str(vector, "file_url").to_owned(), 166 reply_to: None, 167 subject: None, 168 file_type: input_str(vector, "file_type").to_owned(), 169 encryption_algorithm: input_str(vector, "encryption_algorithm").to_owned(), 170 decryption_key: input_str(vector, "decryption_key").to_owned(), 171 decryption_nonce: input_str(vector, "decryption_nonce").to_owned(), 172 encrypted_hash: input_str(vector, "encrypted_hash").to_owned(), 173 original_hash: None, 174 size: Some(input_u64(vector, "size")), 175 dimensions: Some(MessageFileDimensions { 176 w: input_u64(vector, "width") as u32, 177 h: input_u64(vector, "height") as u32, 178 }), 179 blurhash: None, 180 thumb: None, 181 fallbacks: Vec::new(), 182 }; 183 let events = wrap_message_file(&sender, &message, options(vector)) 184 .await 185 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 186 assert_eq!(events.len(), expected_u64(vector, "event_count") as usize); 187 188 let rumor = unwrap_gift_wrap(&receiver, &events[0]) 189 .await 190 .unwrap_or_else(|error| panic!("{} failed: {error}", vector.id)); 191 let rendered = format!("{rumor:?}"); 192 for private_value in [ 193 message.file_url.as_str(), 194 message.decryption_key.as_str(), 195 message.decryption_nonce.as_str(), 196 message.encrypted_hash.as_str(), 197 FIXTURE_ALICE_PUBLIC_KEY_HEX, 198 FIXTURE_BOB_PUBLIC_KEY_HEX, 199 ] { 200 assert!(!rendered.contains(private_value), "{}", vector.id); 201 } 202 match rumor { 203 Rumor::MessageFile(metadata) => { 204 assert_eq!( 205 metadata.author, 206 expected_str(vector, "author"), 207 "{}", 208 vector.id 209 ); 210 assert_eq!( 211 metadata.published_at, 212 expected_u64(vector, "published_at"), 213 "{}", 214 vector.id 215 ); 216 assert_eq!( 217 metadata.data.file_url, 218 expected_str(vector, "file_url"), 219 "{}", 220 vector.id 221 ); 222 assert_eq!( 223 metadata.data.file_type, 224 expected_str(vector, "file_type"), 225 "{}", 226 vector.id 227 ); 228 assert_eq!( 229 metadata.data.encrypted_hash, 230 expected_str(vector, "encrypted_hash"), 231 "{}", 232 vector.id 233 ); 234 assert_eq!( 235 metadata.data.size, 236 Some(expected_u64(vector, "size")), 237 "{}", 238 vector.id 239 ); 240 assert_eq!( 241 metadata.data.dimensions, 242 Some(MessageFileDimensions { 243 w: expected_u64(vector, "width") as u32, 244 h: expected_u64(vector, "height") as u32, 245 }), 246 "{}", 247 vector.id 248 ); 249 } 250 other => panic!("{} expected a message-file rumor, got {other:?}", vector.id), 251 } 252 } 253 254 async fn execute_invalid_recipient(vector: &Vector) { 255 let sender = sender_keys(); 256 let recipient = input_str(vector, "recipient_public_key"); 257 let message = Message { 258 recipients: vec![MessageRecipient { 259 public_key: recipient.to_owned(), 260 relay_url: None, 261 }], 262 content: input_str(vector, "content").to_owned(), 263 reply_to: None, 264 subject: None, 265 }; 266 let error = wrap_message(&sender, &message, options(vector)) 267 .await 268 .expect_err("invalid recipient vector must fail"); 269 assert_eq!(error.code(), expected_str(vector, "error"), "{}", vector.id); 270 let rendered = format!("{error:?} {error}"); 271 assert!(!rendered.contains(recipient), "{}", vector.id); 272 assert!(!rendered.contains(&message.content), "{}", vector.id); 273 } 274 275 fn options(vector: &Vector) -> WrapOptions { 276 WrapOptions::default() 277 .include_sender(false) 278 .with_rumor_created_at(input_u64(vector, "created_at")) 279 } 280 281 fn sender_keys() -> Keys { 282 let keys = Keys::new( 283 SecretKey::from_hex(FIXTURE_ALICE_SECRET_KEY_HEX).expect("approved sender fixture key"), 284 ); 285 assert_eq!(keys.public_key().to_string(), FIXTURE_ALICE_PUBLIC_KEY_HEX); 286 keys 287 } 288 289 fn receiver_keys() -> Keys { 290 let keys = Keys::new( 291 SecretKey::from_hex(FIXTURE_BOB_SECRET_KEY_HEX).expect("approved receiver fixture key"), 292 ); 293 assert_eq!(keys.public_key().to_string(), FIXTURE_BOB_PUBLIC_KEY_HEX); 294 keys 295 } 296 297 fn input_str<'a>(vector: &'a Vector, field: &str) -> &'a str { 298 vector 299 .input 300 .get(field) 301 .and_then(Value::as_str) 302 .unwrap_or_else(|| panic!("{} missing string input {field}", vector.id)) 303 } 304 305 fn input_u64(vector: &Vector, field: &str) -> u64 { 306 vector 307 .input 308 .get(field) 309 .and_then(Value::as_u64) 310 .unwrap_or_else(|| panic!("{} missing u64 input {field}", vector.id)) 311 } 312 313 fn expected_str<'a>(vector: &'a Vector, field: &str) -> &'a str { 314 vector 315 .expected 316 .get(field) 317 .and_then(Value::as_str) 318 .unwrap_or_else(|| panic!("{} missing string expectation {field}", vector.id)) 319 } 320 321 fn expected_u64(vector: &Vector, field: &str) -> u64 { 322 vector 323 .expected 324 .get(field) 325 .and_then(Value::as_u64) 326 .unwrap_or_else(|| panic!("{} missing u64 expectation {field}", vector.id)) 327 }