lib

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

mod.rs (18679B)


      1 pub mod decode;
      2 pub mod encode;
      3 
      4 #[cfg(test)]
      5 mod tests {
      6     use radroots_event::{
      7         envelope::kind::KIND_POST,
      8         farm::crdt::FarmCrdtDocumentKind,
      9         farm::file::{
     10             FarmFileDimensions, FarmFileMetadata, FarmFileSource, KIND_FARM_FILE_METADATA,
     11         },
     12         farm::workspace::FarmWorkspaceRef,
     13     };
     14 
     15     use crate::error::{EventEncodeError, EventParseError};
     16     use crate::farm_file::decode::{
     17         data_from_event, farm_file_metadata_from_event, parsed_from_event,
     18     };
     19     use crate::farm_file::encode::{
     20         farm_file_metadata_build_tags, to_wire_parts, to_wire_parts_with_kind,
     21     };
     22 
     23     const FILE_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ";
     24     const WORKSPACE_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
     25     const OWNER_DOCUMENT_ID: &str = "AAAAAAAAAAAAAAAAAAAAAg";
     26     const GROUP_ID: &str = "field-group";
     27     const EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
     28     const AUTHOR: &str = crate::test_fixtures::FIXTURE_ALICE_PUBLIC_KEY_HEX;
     29     const EVENT_SIG: &str = concat!(
     30         "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
     31         "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
     32     );
     33     const SHA256: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
     34 
     35     #[test]
     36     fn farm_file_metadata_encodes_tags_and_caption_content() {
     37         let metadata = sample_metadata();
     38         let parts = to_wire_parts(&metadata).expect("file metadata wire parts");
     39 
     40         assert_eq!(parts.kind, KIND_FARM_FILE_METADATA);
     41         assert_eq!(parts.content, "Tomatoes harvested from Patch Y.");
     42         assert!(parts.tags.contains(&tag("d", FILE_D_TAG)));
     43         assert!(parts.tags.contains(&tag("h", GROUP_ID)));
     44         assert!(
     45             parts
     46                 .tags
     47                 .contains(&tag("a", "30078:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA"))
     48         );
     49         assert!(
     50             parts
     51                 .tags
     52                 .contains(&tag("url", "https://media.example.invalid/blob/sha256"))
     53         );
     54         assert!(parts.tags.contains(&tag("m", "image/jpeg")));
     55         assert!(parts.tags.contains(&tag("x", SHA256)));
     56 
     57         let decoded = farm_file_metadata_from_event(parts.kind, &parts.tags, &parts.content)
     58             .expect("file metadata decode");
     59         assert_eq!(decoded, metadata);
     60     }
     61 
     62     #[test]
     63     fn farm_file_metadata_rejects_missing_x_bad_hash_and_missing_url() {
     64         let parts = to_wire_parts(&sample_metadata()).expect("file metadata wire parts");
     65         let without_x = parts
     66             .tags
     67             .iter()
     68             .filter(|tag| tag.first().map(|value| value.as_str()) != Some("x"))
     69             .cloned()
     70             .collect::<Vec<_>>();
     71         let missing_x =
     72             farm_file_metadata_from_event(parts.kind, &without_x, &parts.content).unwrap_err();
     73         assert!(matches!(missing_x, EventParseError::MissingTag("x")));
     74 
     75         let mut bad_hash = sample_metadata();
     76         bad_hash.sha256 = "ABC".to_string();
     77         let hash_err = farm_file_metadata_build_tags(&bad_hash).unwrap_err();
     78         assert!(matches!(hash_err, EventEncodeError::InvalidField("sha256")));
     79 
     80         let mut missing_url = sample_metadata();
     81         missing_url.url.clear();
     82         let url_err = to_wire_parts(&missing_url).unwrap_err();
     83         assert!(matches!(
     84             url_err,
     85             EventEncodeError::EmptyRequiredField("url")
     86         ));
     87     }
     88 
     89     #[test]
     90     fn farm_file_metadata_rejects_d_mismatch_and_kind_mismatch() {
     91         let parts = to_wire_parts(&sample_metadata()).expect("file metadata wire parts");
     92         let mut duplicate_d = parts.tags.clone();
     93         duplicate_d.push(vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAw".to_string()]);
     94         let mismatch =
     95             farm_file_metadata_from_event(parts.kind, &duplicate_d, &parts.content).unwrap_err();
     96         assert!(matches!(mismatch, EventParseError::InvalidTag("d")));
     97 
     98         let wrong_kind = to_wire_parts_with_kind(&sample_metadata(), KIND_POST).unwrap_err();
     99         assert!(matches!(
    100             wrong_kind,
    101             EventEncodeError::InvalidKind(KIND_POST)
    102         ));
    103 
    104         let decode_wrong_kind =
    105             farm_file_metadata_from_event(KIND_POST, &parts.tags, &parts.content).unwrap_err();
    106         assert!(matches!(
    107             decode_wrong_kind,
    108             EventParseError::InvalidKind {
    109                 expected: "1063",
    110                 got: KIND_POST
    111             }
    112         ));
    113     }
    114 
    115     #[test]
    116     fn farm_file_metadata_decodes_empty_content_as_absent_caption() {
    117         let mut metadata = sample_metadata();
    118         metadata.caption = None;
    119         let parts = to_wire_parts(&metadata).expect("file metadata wire parts");
    120 
    121         assert_eq!(parts.content, "");
    122         let decoded =
    123             farm_file_metadata_from_event(parts.kind, &parts.tags, "").expect("file decode");
    124         assert_eq!(decoded.caption, None);
    125     }
    126 
    127     #[test]
    128     fn farm_file_metadata_wrappers_roundtrip_minimal_optional_shape() {
    129         let mut metadata = sample_metadata();
    130         metadata.caption = None;
    131         metadata.original_sha256 = None;
    132         metadata.size_bytes = None;
    133         metadata.dimensions = None;
    134         metadata.blurhash = None;
    135         metadata.thumb = None;
    136         metadata.image = Some(FarmFileSource {
    137             url: "https://media.example.invalid/image/sha256".to_string(),
    138             mime_type: None,
    139             dimensions: Some(FarmFileDimensions { w: 640, h: 480 }),
    140         });
    141         metadata.alt = None;
    142         metadata.fallbacks.clear();
    143         let parts = to_wire_parts(&metadata).expect("file metadata wire parts");
    144 
    145         assert!(
    146             !parts
    147                 .tags
    148                 .iter()
    149                 .any(|tag| tag.first().map(String::as_str) == Some("size"))
    150         );
    151         assert!(
    152             !parts
    153                 .tags
    154                 .iter()
    155                 .any(|tag| tag.first().map(String::as_str) == Some("dim"))
    156         );
    157         assert!(parts.tags.iter().any(|tag| tag
    158             == &vec![
    159                 "image".to_string(),
    160                 "https://media.example.invalid/image/sha256".to_string(),
    161                 "640x480".to_string()
    162             ]));
    163 
    164         let data = data_from_event(
    165             EVENT_ID.to_string(),
    166             AUTHOR.to_string(),
    167             42,
    168             parts.kind,
    169             parts.content.clone(),
    170             parts.tags.clone(),
    171         )
    172         .expect("parsed data");
    173         assert_eq!(data.id, EVENT_ID);
    174         assert_eq!(data.author, AUTHOR);
    175         assert_eq!(data.published_at, 42);
    176         assert_eq!(data.kind, KIND_FARM_FILE_METADATA);
    177         assert_eq!(data.data, metadata);
    178 
    179         let err = parsed_from_event(
    180             EVENT_ID.to_string(),
    181             AUTHOR.to_string(),
    182             42,
    183             KIND_POST,
    184             parts.content.clone(),
    185             parts.tags.clone(),
    186             EVENT_SIG.to_string(),
    187         )
    188         .unwrap_err();
    189         assert!(matches!(
    190             err,
    191             EventParseError::InvalidKind {
    192                 expected: "1063",
    193                 got: KIND_POST
    194             }
    195         ));
    196 
    197         let parsed = parsed_from_event(
    198             EVENT_ID.to_string(),
    199             AUTHOR.to_string(),
    200             42,
    201             parts.kind,
    202             parts.content,
    203             parts.tags,
    204             EVENT_SIG.to_string(),
    205         )
    206         .expect("parsed event");
    207         assert_eq!(parsed.event.signature_hex(), EVENT_SIG);
    208         assert_eq!(parsed.data.data, metadata);
    209     }
    210 
    211     #[test]
    212     fn farm_file_metadata_preserves_expanded_owner_document_kinds() {
    213         for kind in [
    214             FarmCrdtDocumentKind::FarmMembership,
    215             FarmCrdtDocumentKind::FarmRolePolicy,
    216             FarmCrdtDocumentKind::FarmActivity,
    217             FarmCrdtDocumentKind::FarmLocation,
    218             FarmCrdtDocumentKind::FarmCrop,
    219             FarmCrdtDocumentKind::FarmCropVariety,
    220             FarmCrdtDocumentKind::FarmCropCycle,
    221             FarmCrdtDocumentKind::FarmAttachment,
    222             FarmCrdtDocumentKind::FarmPayPeriod,
    223             FarmCrdtDocumentKind::Other {
    224                 value: "FarmSoilTest".to_string(),
    225             },
    226         ] {
    227             let mut metadata = sample_metadata();
    228             metadata.owner_document_kind = kind;
    229             let parts = to_wire_parts(&metadata).expect("file metadata wire parts");
    230             let decoded = farm_file_metadata_from_event(parts.kind, &parts.tags, &parts.content)
    231                 .expect("file metadata decode");
    232 
    233             assert_eq!(decoded.owner_document_kind, metadata.owner_document_kind);
    234         }
    235     }
    236 
    237     #[test]
    238     #[cfg_attr(coverage_nightly, coverage(off))]
    239     fn farm_file_metadata_rejects_malformed_decode_tags() {
    240         let parts = to_wire_parts(&sample_metadata()).expect("file metadata wire parts");
    241 
    242         let mut missing_owner = parts.tags.clone();
    243         missing_owner
    244             .retain(|tag| tag.first().map(String::as_str) != Some("radroots:owner_document"));
    245         let err =
    246             farm_file_metadata_from_event(parts.kind, &missing_owner, &parts.content).unwrap_err();
    247         assert!(matches!(
    248             err,
    249             EventParseError::MissingTag("radroots:owner_document")
    250         ));
    251 
    252         for replacement in [
    253             vec![
    254                 "radroots:owner_document".to_string(),
    255                 OWNER_DOCUMENT_ID.to_string(),
    256             ],
    257             vec![
    258                 "radroots:owner_document".to_string(),
    259                 OWNER_DOCUMENT_ID.to_string(),
    260                 " ".to_string(),
    261             ],
    262             vec![
    263                 "radroots:owner_document".to_string(),
    264                 "bad d tag".to_string(),
    265                 "FarmTask".to_string(),
    266             ],
    267         ] {
    268             let mut tags = replace_tag(&parts.tags, "radroots:owner_document", replacement);
    269             let err = farm_file_metadata_from_event(parts.kind, &tags, &parts.content).unwrap_err();
    270             assert!(matches!(
    271                 err,
    272                 EventParseError::InvalidTag("radroots:owner_document")
    273             ));
    274             tags.clear();
    275         }
    276 
    277         for (key, value, expected) in [
    278             ("size", "not-a-number", "size"),
    279             ("dim", "bad", "dim"),
    280             ("dim", "badx12", "dim"),
    281             ("dim", "12xbad", "dim"),
    282             ("dim", "0x12", "dim"),
    283             ("dim", "12x0", "dim"),
    284             ("thumb", "", "thumb"),
    285             ("thumb", " ", "thumb"),
    286         ] {
    287             let tags = replace_tag(&parts.tags, key, tag(key, value));
    288             let err = farm_file_metadata_from_event(parts.kind, &tags, &parts.content).unwrap_err();
    289             match err {
    290                 EventParseError::InvalidTag(found) | EventParseError::InvalidNumber(found, _) => {
    291                     assert_eq!(found, expected);
    292                 }
    293                 other => panic!("unexpected error: {other:?}"),
    294             }
    295         }
    296 
    297         for replacement in [
    298             vec!["thumb".to_string()],
    299             vec![
    300                 "thumb".to_string(),
    301                 "https://media.example.invalid/thumb/sha256".to_string(),
    302                 "image/jpeg".to_string(),
    303                 "320x240".to_string(),
    304                 "extra".to_string(),
    305             ],
    306             vec![
    307                 "thumb".to_string(),
    308                 "https://media.example.invalid/thumb/sha256".to_string(),
    309                 "image/jpeg".to_string(),
    310                 " ".to_string(),
    311             ],
    312         ] {
    313             let tags = replace_tag(&parts.tags, "thumb", replacement);
    314             let err = farm_file_metadata_from_event(parts.kind, &tags, &parts.content).unwrap_err();
    315             assert!(matches!(err, EventParseError::InvalidTag("thumb")));
    316         }
    317 
    318         let tags = replace_tag(
    319             &parts.tags,
    320             "thumb",
    321             vec![
    322                 "thumb".to_string(),
    323                 "https://media.example.invalid/thumb/sha256".to_string(),
    324                 "320x240".to_string(),
    325             ],
    326         );
    327         let decoded =
    328             farm_file_metadata_from_event(parts.kind, &tags, &parts.content).expect("metadata");
    329         assert_eq!(
    330             decoded
    331                 .thumb
    332                 .as_ref()
    333                 .and_then(|source| source.mime_type.as_deref()),
    334             None
    335         );
    336         assert_eq!(
    337             decoded.thumb.and_then(|source| source.dimensions),
    338             Some(FarmFileDimensions { w: 320, h: 240 })
    339         );
    340 
    341         let tags = replace_tag(
    342             &parts.tags,
    343             "thumb",
    344             vec![
    345                 "thumb".to_string(),
    346                 "https://media.example.invalid/thumb/sha256".to_string(),
    347             ],
    348         );
    349         let decoded =
    350             farm_file_metadata_from_event(parts.kind, &tags, &parts.content).expect("metadata");
    351         assert_eq!(decoded.thumb.and_then(|source| source.dimensions), None);
    352 
    353         let err = farm_file_metadata_from_event(parts.kind, &parts.tags, " ").unwrap_err();
    354         assert!(matches!(err, EventParseError::InvalidTag("caption")));
    355     }
    356 
    357     #[test]
    358     fn farm_file_metadata_rejects_encoder_validation_edges() {
    359         for (metadata, expected) in [
    360             {
    361                 let mut metadata = sample_metadata();
    362                 metadata.workspace.d_tag = "bad d tag".to_string();
    363                 (metadata, EventEncodeError::InvalidField("workspace.d_tag"))
    364             },
    365             {
    366                 let mut metadata = sample_metadata();
    367                 metadata.caption = Some("".to_string());
    368                 (metadata, EventEncodeError::EmptyRequiredField("caption"))
    369             },
    370             {
    371                 let mut metadata = sample_metadata();
    372                 metadata.dimensions = Some(FarmFileDimensions { w: 0, h: 1200 });
    373                 (metadata, EventEncodeError::InvalidField("dimensions"))
    374             },
    375             {
    376                 let mut metadata = sample_metadata();
    377                 metadata.blurhash = Some("".to_string());
    378                 (metadata, EventEncodeError::EmptyRequiredField("blurhash"))
    379             },
    380             {
    381                 let mut metadata = sample_metadata();
    382                 metadata.thumb = Some(FarmFileSource {
    383                     url: "".to_string(),
    384                     mime_type: None,
    385                     dimensions: None,
    386                 });
    387                 (metadata, EventEncodeError::EmptyRequiredField("thumb"))
    388             },
    389             {
    390                 let mut metadata = sample_metadata();
    391                 metadata.thumb = Some(FarmFileSource {
    392                     url: "https://media.example.invalid/thumb/sha256".to_string(),
    393                     mime_type: Some("".to_string()),
    394                     dimensions: None,
    395                 });
    396                 (metadata, EventEncodeError::EmptyRequiredField("thumb"))
    397             },
    398             {
    399                 let mut metadata = sample_metadata();
    400                 metadata.thumb = Some(FarmFileSource {
    401                     url: "https://media.example.invalid/thumb/sha256".to_string(),
    402                     mime_type: None,
    403                     dimensions: Some(FarmFileDimensions { w: 320, h: 0 }),
    404                 });
    405                 (metadata, EventEncodeError::InvalidField("thumb"))
    406             },
    407             {
    408                 let mut metadata = sample_metadata();
    409                 metadata.alt = Some("".to_string());
    410                 (metadata, EventEncodeError::EmptyRequiredField("alt"))
    411             },
    412             {
    413                 let mut metadata = sample_metadata();
    414                 metadata.fallbacks = vec!["".to_string()];
    415                 (metadata, EventEncodeError::EmptyRequiredField("fallbacks"))
    416             },
    417         ] {
    418             let err = farm_file_metadata_build_tags(&metadata).unwrap_err();
    419             assert_same_encode_error(err, expected);
    420         }
    421     }
    422 
    423     fn sample_metadata() -> FarmFileMetadata {
    424         FarmFileMetadata {
    425             d_tag: FILE_D_TAG.to_string(),
    426             workspace: FarmWorkspaceRef {
    427                 pubkey: "workspace_pubkey".to_string(),
    428                 d_tag: WORKSPACE_D_TAG.to_string(),
    429             },
    430             farm_group_id: GROUP_ID.to_string(),
    431             owner_document_id: OWNER_DOCUMENT_ID.to_string(),
    432             owner_document_kind: FarmCrdtDocumentKind::FarmTask,
    433             caption: Some("Tomatoes harvested from Patch Y.".to_string()),
    434             url: "https://media.example.invalid/blob/sha256".to_string(),
    435             mime_type: "image/jpeg".to_string(),
    436             sha256: SHA256.to_string(),
    437             original_sha256: Some(
    438                 "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string(),
    439             ),
    440             size_bytes: Some(123_456),
    441             dimensions: Some(FarmFileDimensions { w: 1600, h: 1200 }),
    442             blurhash: Some("LEHV6nWB2yk8pyo0adR*.7kCMdnj".to_string()),
    443             thumb: Some(FarmFileSource {
    444                 url: "https://media.example.invalid/thumb/sha256".to_string(),
    445                 mime_type: Some("image/jpeg".to_string()),
    446                 dimensions: Some(FarmFileDimensions { w: 320, h: 240 }),
    447             }),
    448             image: None,
    449             alt: Some("Harvested tomatoes in a crate".to_string()),
    450             fallbacks: vec!["https://fallback.example.invalid/blob/sha256".to_string()],
    451         }
    452     }
    453 
    454     fn tag(key: &str, value: &str) -> Vec<String> {
    455         vec![key.to_string(), value.to_string()]
    456     }
    457 
    458     fn replace_tag(tags: &[Vec<String>], key: &str, replacement: Vec<String>) -> Vec<Vec<String>> {
    459         tags.iter()
    460             .map(|tag| {
    461                 if tag.first().map(String::as_str) == Some(key) {
    462                     replacement.clone()
    463                 } else {
    464                     tag.clone()
    465                 }
    466             })
    467             .collect()
    468     }
    469 
    470     #[cfg_attr(coverage_nightly, coverage(off))]
    471     fn assert_same_encode_error(actual: EventEncodeError, expected: EventEncodeError) {
    472         match (actual, expected) {
    473             (
    474                 EventEncodeError::EmptyRequiredField(actual),
    475                 EventEncodeError::EmptyRequiredField(expected),
    476             )
    477             | (EventEncodeError::InvalidField(actual), EventEncodeError::InvalidField(expected)) => {
    478                 assert_eq!(actual, expected);
    479             }
    480             (EventEncodeError::InvalidKind(actual), EventEncodeError::InvalidKind(expected)) => {
    481                 assert_eq!(actual, expected);
    482             }
    483             (EventEncodeError::Json, EventEncodeError::Json) => {}
    484             (actual, expected) => panic!("unexpected error {actual:?}, expected {expected:?}"),
    485         }
    486     }
    487 }