message_file.rs (19763B)
1 #[path = "../src/test_fixtures.rs"] 2 mod test_fixtures; 3 4 use radroots_events::RadrootsNostrEventPtr; 5 use radroots_events::kinds::{KIND_MESSAGE, KIND_MESSAGE_FILE}; 6 use radroots_events::message::RadrootsMessageRecipient; 7 use radroots_events::message_file::{RadrootsMessageFile, RadrootsMessageFileDimensions}; 8 9 use radroots_events_codec::error::{EventEncodeError, EventParseError}; 10 use radroots_events_codec::message_file::decode::{ 11 data_from_event, message_file_from_tags, parsed_from_event, 12 }; 13 use radroots_events_codec::message_file::encode::{ 14 message_file_build_tags, to_wire_parts, to_wire_parts_with_kind, 15 }; 16 use test_fixtures::{CDN_PRIMARY_HTTPS, RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS}; 17 18 fn file_url(path: &str) -> String { 19 format!("{CDN_PRIMARY_HTTPS}/{path}") 20 } 21 22 fn sample_message_file() -> RadrootsMessageFile { 23 RadrootsMessageFile { 24 recipients: vec![ 25 RadrootsMessageRecipient { 26 public_key: "pub1".to_string(), 27 relay_url: None, 28 }, 29 RadrootsMessageRecipient { 30 public_key: "pub2".to_string(), 31 relay_url: Some(RELAY_PRIMARY_WSS.to_string()), 32 }, 33 ], 34 file_url: file_url("encrypted.bin"), 35 reply_to: Some(RadrootsNostrEventPtr { 36 id: "reply".to_string(), 37 relays: Some(RELAY_SECONDARY_WSS.to_string()), 38 }), 39 subject: Some("topic".to_string()), 40 file_type: "image/jpeg".to_string(), 41 encryption_algorithm: "aes-gcm".to_string(), 42 decryption_key: "key".to_string(), 43 decryption_nonce: "nonce".to_string(), 44 encrypted_hash: "hash".to_string(), 45 original_hash: Some("orig-hash".to_string()), 46 size: Some(1200), 47 dimensions: Some(RadrootsMessageFileDimensions { w: 1200, h: 800 }), 48 blurhash: Some("blurhash".to_string()), 49 thumb: Some(file_url("thumb.bin")), 50 fallbacks: vec![file_url("fallback-1.bin"), file_url("fallback-2.bin")], 51 } 52 } 53 54 fn minimal_message_file_tags() -> Vec<Vec<String>> { 55 vec![ 56 vec!["p".to_string(), "pub1".to_string()], 57 vec!["file-type".to_string(), "image/jpeg".to_string()], 58 vec!["encryption-algorithm".to_string(), "aes-gcm".to_string()], 59 vec!["decryption-key".to_string(), "key".to_string()], 60 vec!["decryption-nonce".to_string(), "nonce".to_string()], 61 vec!["x".to_string(), "hash".to_string()], 62 ] 63 } 64 65 #[test] 66 fn message_file_build_tags_requires_recipients() { 67 let mut message = sample_message_file(); 68 message.recipients.clear(); 69 70 let err = message_file_build_tags(&message).unwrap_err(); 71 assert!(matches!( 72 err, 73 EventEncodeError::EmptyRequiredField("recipients") 74 )); 75 } 76 77 #[test] 78 fn message_file_to_wire_parts_requires_file_url() { 79 let mut message = sample_message_file(); 80 message.file_url = " ".to_string(); 81 82 let err = to_wire_parts(&message).unwrap_err(); 83 assert!(matches!( 84 err, 85 EventEncodeError::EmptyRequiredField("file_url") 86 )); 87 } 88 89 #[test] 90 fn message_file_to_wire_parts_propagates_tag_build_errors() { 91 let mut message = sample_message_file(); 92 message.file_type = " ".to_string(); 93 let err = to_wire_parts(&message).unwrap_err(); 94 assert!(matches!( 95 err, 96 EventEncodeError::EmptyRequiredField("file_type") 97 )); 98 } 99 100 #[test] 101 fn message_file_build_tags_requires_file_type() { 102 let mut message = sample_message_file(); 103 message.file_type = " ".to_string(); 104 105 let err = message_file_build_tags(&message).unwrap_err(); 106 assert!(matches!( 107 err, 108 EventEncodeError::EmptyRequiredField("file_type") 109 )); 110 } 111 112 #[test] 113 fn message_file_build_tags_requires_crypto_fields() { 114 let mut message = sample_message_file(); 115 message.encryption_algorithm = " ".to_string(); 116 let err = message_file_build_tags(&message).unwrap_err(); 117 assert!(matches!( 118 err, 119 EventEncodeError::EmptyRequiredField("encryption_algorithm") 120 )); 121 122 let mut message = sample_message_file(); 123 message.decryption_key = " ".to_string(); 124 let err = message_file_build_tags(&message).unwrap_err(); 125 assert!(matches!( 126 err, 127 EventEncodeError::EmptyRequiredField("decryption_key") 128 )); 129 130 let mut message = sample_message_file(); 131 message.decryption_nonce = " ".to_string(); 132 let err = message_file_build_tags(&message).unwrap_err(); 133 assert!(matches!( 134 err, 135 EventEncodeError::EmptyRequiredField("decryption_nonce") 136 )); 137 138 let mut message = sample_message_file(); 139 message.encrypted_hash = " ".to_string(); 140 let err = message_file_build_tags(&message).unwrap_err(); 141 assert!(matches!( 142 err, 143 EventEncodeError::EmptyRequiredField("encrypted_hash") 144 )); 145 } 146 147 #[test] 148 fn message_file_build_tags_rejects_invalid_reply_subject_and_fallbacks() { 149 let mut message = sample_message_file(); 150 message.reply_to = Some(RadrootsNostrEventPtr { 151 id: " ".to_string(), 152 relays: None, 153 }); 154 let err = message_file_build_tags(&message).unwrap_err(); 155 assert!(matches!( 156 err, 157 EventEncodeError::EmptyRequiredField("reply_to.id") 158 )); 159 160 let mut message = sample_message_file(); 161 message.subject = Some(" ".to_string()); 162 let err = message_file_build_tags(&message).unwrap_err(); 163 assert!(matches!( 164 err, 165 EventEncodeError::EmptyRequiredField("subject") 166 )); 167 168 let mut message = sample_message_file(); 169 message.fallbacks = vec![" ".to_string()]; 170 let err = message_file_build_tags(&message).unwrap_err(); 171 assert!(matches!( 172 err, 173 EventEncodeError::EmptyRequiredField("fallback") 174 )); 175 } 176 177 #[test] 178 fn message_file_to_wire_parts_with_kind_enforces_kind() { 179 let message = sample_message_file(); 180 let parts = to_wire_parts_with_kind(&message, KIND_MESSAGE_FILE).unwrap(); 181 assert_eq!(parts.kind, KIND_MESSAGE_FILE); 182 183 let err = to_wire_parts_with_kind(&message, KIND_MESSAGE).unwrap_err(); 184 assert!(matches!(err, EventEncodeError::InvalidKind(KIND_MESSAGE))); 185 } 186 187 #[test] 188 fn message_file_to_wire_parts_sets_kind_content_and_tags() { 189 let message = sample_message_file(); 190 let parts = to_wire_parts(&message).unwrap(); 191 192 assert_eq!(parts.kind, KIND_MESSAGE_FILE); 193 assert_eq!(parts.content, message.file_url); 194 assert_eq!( 195 parts.tags, 196 vec![ 197 vec!["p".to_string(), "pub1".to_string()], 198 vec![ 199 "p".to_string(), 200 "pub2".to_string(), 201 RELAY_PRIMARY_WSS.to_string() 202 ], 203 vec![ 204 "e".to_string(), 205 "reply".to_string(), 206 RELAY_SECONDARY_WSS.to_string() 207 ], 208 vec!["subject".to_string(), "topic".to_string()], 209 vec!["file-type".to_string(), "image/jpeg".to_string()], 210 vec!["encryption-algorithm".to_string(), "aes-gcm".to_string()], 211 vec!["decryption-key".to_string(), "key".to_string()], 212 vec!["decryption-nonce".to_string(), "nonce".to_string()], 213 vec!["x".to_string(), "hash".to_string()], 214 vec!["ox".to_string(), "orig-hash".to_string()], 215 vec!["size".to_string(), "1200".to_string()], 216 vec!["dim".to_string(), "1200x800".to_string()], 217 vec!["blurhash".to_string(), "blurhash".to_string()], 218 vec!["thumb".to_string(), file_url("thumb.bin")], 219 vec!["fallback".to_string(), file_url("fallback-1.bin")], 220 vec!["fallback".to_string(), file_url("fallback-2.bin")], 221 ] 222 ); 223 } 224 225 #[test] 226 fn message_file_roundtrip_from_tags() { 227 let message = sample_message_file(); 228 let parts = to_wire_parts(&message).unwrap(); 229 230 let decoded = message_file_from_tags(parts.kind, &parts.tags, &parts.content).unwrap(); 231 assert_eq!(decoded.file_url, message.file_url); 232 assert_eq!(decoded.file_type, message.file_type); 233 assert_eq!(decoded.encryption_algorithm, message.encryption_algorithm); 234 assert_eq!(decoded.decryption_key, message.decryption_key); 235 assert_eq!(decoded.decryption_nonce, message.decryption_nonce); 236 assert_eq!(decoded.encrypted_hash, message.encrypted_hash); 237 assert_eq!(decoded.original_hash, message.original_hash); 238 assert_eq!(decoded.size, message.size); 239 assert_eq!(decoded.dimensions, message.dimensions); 240 assert_eq!(decoded.blurhash, message.blurhash); 241 assert_eq!(decoded.thumb, message.thumb); 242 assert_eq!(decoded.fallbacks, message.fallbacks); 243 assert_eq!(decoded.recipients.len(), message.recipients.len()); 244 } 245 246 #[test] 247 fn message_file_from_tags_rejects_wrong_kind() { 248 let message = sample_message_file(); 249 let parts = to_wire_parts(&message).unwrap(); 250 251 let err = message_file_from_tags(KIND_MESSAGE, &parts.tags, &parts.content).unwrap_err(); 252 assert!(matches!( 253 err, 254 EventParseError::InvalidKind { 255 expected: "15", 256 got: KIND_MESSAGE 257 } 258 )); 259 } 260 261 #[test] 262 fn message_file_from_tags_rejects_invalid_optional_tags() { 263 let message = sample_message_file(); 264 let mut parts = to_wire_parts(&message).unwrap(); 265 let size_tag = parts 266 .tags 267 .iter_mut() 268 .find(|tag| tag.first().map(|value| value.as_str()) == Some("size")) 269 .expect("size tag"); 270 size_tag[1] = "not-a-number".to_string(); 271 let err = message_file_from_tags(KIND_MESSAGE_FILE, &parts.tags, &parts.content).unwrap_err(); 272 assert!(matches!(err, EventParseError::InvalidNumber("size", _))); 273 274 let err = message_file_from_tags( 275 KIND_MESSAGE_FILE, 276 &[ 277 vec!["p".to_string(), "pub1".to_string()], 278 vec!["file-type".to_string(), "image/jpeg".to_string()], 279 vec!["encryption-algorithm".to_string(), "aes-gcm".to_string()], 280 vec!["decryption-key".to_string(), "key".to_string()], 281 vec!["decryption-nonce".to_string(), "nonce".to_string()], 282 vec!["x".to_string(), "hash".to_string()], 283 vec!["dim".to_string(), "10".to_string()], 284 ], 285 &file_url("encrypted.bin"), 286 ) 287 .unwrap_err(); 288 assert!(matches!(err, EventParseError::InvalidTag("dim"))); 289 290 let err = message_file_from_tags( 291 KIND_MESSAGE_FILE, 292 &[ 293 vec!["p".to_string(), "pub1".to_string()], 294 vec!["file-type".to_string(), "image/jpeg".to_string()], 295 vec!["encryption-algorithm".to_string(), "aes-gcm".to_string()], 296 vec!["decryption-key".to_string(), "key".to_string()], 297 vec!["decryption-nonce".to_string(), "nonce".to_string()], 298 vec!["x".to_string(), "hash".to_string()], 299 vec!["fallback".to_string()], 300 ], 301 &file_url("encrypted.bin"), 302 ) 303 .unwrap_err(); 304 assert!(matches!(err, EventParseError::InvalidTag("fallback"))); 305 306 let err = message_file_from_tags( 307 KIND_MESSAGE_FILE, 308 &[ 309 vec!["p".to_string(), "pub1".to_string()], 310 vec!["file-type".to_string(), " ".to_string()], 311 vec!["encryption-algorithm".to_string(), "aes-gcm".to_string()], 312 vec!["decryption-key".to_string(), "key".to_string()], 313 vec!["decryption-nonce".to_string(), "nonce".to_string()], 314 vec!["x".to_string(), "hash".to_string()], 315 ], 316 &file_url("encrypted.bin"), 317 ) 318 .unwrap_err(); 319 assert!(matches!(err, EventParseError::InvalidTag("file-type"))); 320 321 let err = message_file_from_tags( 322 KIND_MESSAGE_FILE, 323 &[ 324 vec!["p".to_string(), "pub1".to_string()], 325 vec!["file-type".to_string(), "image/jpeg".to_string()], 326 vec!["encryption-algorithm".to_string(), "aes-gcm".to_string()], 327 vec!["decryption-key".to_string(), "key".to_string()], 328 vec!["decryption-nonce".to_string(), "nonce".to_string()], 329 vec!["x".to_string(), "hash".to_string()], 330 vec!["size".to_string(), " ".to_string()], 331 ], 332 &file_url("encrypted.bin"), 333 ) 334 .unwrap_err(); 335 assert!(matches!(err, EventParseError::InvalidTag("size"))); 336 337 let err = message_file_from_tags( 338 KIND_MESSAGE_FILE, 339 &[ 340 vec!["p".to_string(), "pub1".to_string()], 341 vec!["file-type".to_string(), "image/jpeg".to_string()], 342 vec!["encryption-algorithm".to_string(), "aes-gcm".to_string()], 343 vec!["decryption-key".to_string(), "key".to_string()], 344 vec!["decryption-nonce".to_string(), "nonce".to_string()], 345 vec!["x".to_string(), "hash".to_string()], 346 vec!["dim".to_string(), " ".to_string()], 347 ], 348 &file_url("encrypted.bin"), 349 ) 350 .unwrap_err(); 351 assert!(matches!(err, EventParseError::InvalidTag("dim"))); 352 353 let err = message_file_from_tags( 354 KIND_MESSAGE_FILE, 355 &[ 356 vec!["p".to_string(), "pub1".to_string()], 357 vec!["file-type".to_string(), "image/jpeg".to_string()], 358 vec!["encryption-algorithm".to_string(), "aes-gcm".to_string()], 359 vec!["decryption-key".to_string(), "key".to_string()], 360 vec!["decryption-nonce".to_string(), "nonce".to_string()], 361 vec!["x".to_string(), "hash".to_string()], 362 vec!["thumb".to_string(), " ".to_string()], 363 ], 364 &file_url("encrypted.bin"), 365 ) 366 .unwrap_err(); 367 assert!(matches!(err, EventParseError::InvalidTag("thumb"))); 368 369 let err = message_file_from_tags( 370 KIND_MESSAGE_FILE, 371 &[ 372 vec!["p".to_string(), "pub1".to_string()], 373 vec!["file-type".to_string(), "image/jpeg".to_string()], 374 vec!["encryption-algorithm".to_string(), "aes-gcm".to_string()], 375 vec!["decryption-key".to_string(), "key".to_string()], 376 vec!["decryption-nonce".to_string(), "nonce".to_string()], 377 vec!["x".to_string(), "hash".to_string()], 378 vec!["fallback".to_string(), " ".to_string()], 379 ], 380 &file_url("encrypted.bin"), 381 ) 382 .unwrap_err(); 383 assert!(matches!(err, EventParseError::InvalidTag("fallback"))); 384 } 385 386 #[test] 387 fn message_file_metadata_and_index_from_event_roundtrip() { 388 let message = sample_message_file(); 389 let parts = to_wire_parts(&message).unwrap(); 390 let metadata = data_from_event( 391 "id".to_string(), 392 "author".to_string(), 393 77, 394 parts.kind, 395 parts.content.clone(), 396 parts.tags.clone(), 397 ) 398 .unwrap(); 399 assert_eq!(metadata.id, "id"); 400 assert_eq!(metadata.author, "author"); 401 assert_eq!(metadata.published_at, 77); 402 assert_eq!(metadata.kind, KIND_MESSAGE_FILE); 403 assert_eq!(metadata.data.file_type, "image/jpeg"); 404 assert_eq!(metadata.data.recipients.len(), 2); 405 406 let index = parsed_from_event( 407 "id".to_string(), 408 "author".to_string(), 409 77, 410 parts.kind, 411 parts.content, 412 parts.tags, 413 "sig".to_string(), 414 ) 415 .unwrap(); 416 assert_eq!(index.event.kind, KIND_MESSAGE_FILE); 417 assert_eq!(index.event.sig, "sig"); 418 assert_eq!(index.data.data.file_type, "image/jpeg"); 419 } 420 421 #[test] 422 fn message_file_index_from_event_propagates_parse_errors() { 423 let err = parsed_from_event( 424 "id".to_string(), 425 "author".to_string(), 426 77, 427 KIND_MESSAGE, 428 "payload".to_string(), 429 Vec::new(), 430 "sig".to_string(), 431 ) 432 .unwrap_err(); 433 assert!(matches!( 434 err, 435 EventParseError::InvalidKind { 436 expected: "15", 437 got: KIND_MESSAGE 438 } 439 )); 440 } 441 442 #[test] 443 fn message_file_from_tags_rejects_empty_content() { 444 let err = message_file_from_tags( 445 KIND_MESSAGE_FILE, 446 &[ 447 vec!["p".to_string(), "pub1".to_string()], 448 vec!["file-type".to_string(), "image/jpeg".to_string()], 449 vec!["encryption-algorithm".to_string(), "aes-gcm".to_string()], 450 vec!["decryption-key".to_string(), "key".to_string()], 451 vec!["decryption-nonce".to_string(), "nonce".to_string()], 452 vec!["x".to_string(), "hash".to_string()], 453 ], 454 " ", 455 ) 456 .unwrap_err(); 457 assert!(matches!(err, EventParseError::InvalidTag("content"))); 458 } 459 460 #[test] 461 fn message_file_from_tags_rejects_more_invalid_tag_shapes() { 462 let mut tags = minimal_message_file_tags(); 463 tags[1].truncate(1); 464 let err = 465 message_file_from_tags(KIND_MESSAGE_FILE, &tags, &file_url("encrypted.bin")).unwrap_err(); 466 assert!(matches!(err, EventParseError::MissingTag("file-type"))); 467 468 let mut tags = minimal_message_file_tags(); 469 tags[0][1] = " ".to_string(); 470 let err = 471 message_file_from_tags(KIND_MESSAGE_FILE, &tags, &file_url("encrypted.bin")).unwrap_err(); 472 assert!(matches!(err, EventParseError::InvalidTag("p"))); 473 474 let mut tags = minimal_message_file_tags(); 475 tags.push(vec!["e".to_string(), " ".to_string()]); 476 let err = 477 message_file_from_tags(KIND_MESSAGE_FILE, &tags, &file_url("encrypted.bin")).unwrap_err(); 478 assert!(matches!(err, EventParseError::InvalidTag("e"))); 479 480 let mut tags = minimal_message_file_tags(); 481 tags.push(vec!["subject".to_string(), " ".to_string()]); 482 let err = 483 message_file_from_tags(KIND_MESSAGE_FILE, &tags, &file_url("encrypted.bin")).unwrap_err(); 484 assert!(matches!(err, EventParseError::InvalidTag("subject"))); 485 486 let mut tags = minimal_message_file_tags(); 487 tags[2][1] = " ".to_string(); 488 let err = 489 message_file_from_tags(KIND_MESSAGE_FILE, &tags, &file_url("encrypted.bin")).unwrap_err(); 490 assert!(matches!( 491 err, 492 EventParseError::InvalidTag("encryption-algorithm") 493 )); 494 495 let mut tags = minimal_message_file_tags(); 496 tags[3][1] = " ".to_string(); 497 let err = 498 message_file_from_tags(KIND_MESSAGE_FILE, &tags, &file_url("encrypted.bin")).unwrap_err(); 499 assert!(matches!(err, EventParseError::InvalidTag("decryption-key"))); 500 501 let mut tags = minimal_message_file_tags(); 502 tags[4][1] = " ".to_string(); 503 let err = 504 message_file_from_tags(KIND_MESSAGE_FILE, &tags, &file_url("encrypted.bin")).unwrap_err(); 505 assert!(matches!( 506 err, 507 EventParseError::InvalidTag("decryption-nonce") 508 )); 509 510 let mut tags = minimal_message_file_tags(); 511 tags[5][1] = " ".to_string(); 512 let err = 513 message_file_from_tags(KIND_MESSAGE_FILE, &tags, &file_url("encrypted.bin")).unwrap_err(); 514 assert!(matches!(err, EventParseError::InvalidTag("x"))); 515 516 let mut tags = minimal_message_file_tags(); 517 tags.push(vec!["ox".to_string(), " ".to_string()]); 518 let err = 519 message_file_from_tags(KIND_MESSAGE_FILE, &tags, &file_url("encrypted.bin")).unwrap_err(); 520 assert!(matches!(err, EventParseError::InvalidTag("ox"))); 521 522 let mut tags = minimal_message_file_tags(); 523 tags.push(vec!["blurhash".to_string(), " ".to_string()]); 524 let err = 525 message_file_from_tags(KIND_MESSAGE_FILE, &tags, &file_url("encrypted.bin")).unwrap_err(); 526 assert!(matches!(err, EventParseError::InvalidTag("blurhash"))); 527 } 528 529 #[test] 530 fn message_file_from_tags_rejects_invalid_dimension_components() { 531 let mut tags = minimal_message_file_tags(); 532 tags.push(vec!["dim".to_string(), "badx10".to_string()]); 533 let err = 534 message_file_from_tags(KIND_MESSAGE_FILE, &tags, &file_url("encrypted.bin")).unwrap_err(); 535 assert!(matches!(err, EventParseError::InvalidTag("dim"))); 536 537 let mut tags = minimal_message_file_tags(); 538 tags.push(vec!["dim".to_string(), "10xbad".to_string()]); 539 let err = 540 message_file_from_tags(KIND_MESSAGE_FILE, &tags, &file_url("encrypted.bin")).unwrap_err(); 541 assert!(matches!(err, EventParseError::InvalidTag("dim"))); 542 }