lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

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 }