lib

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

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 }