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