lib

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

mod.rs (18475B)


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