lib

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

mod.rs (18800B)


      1 pub mod encode;
      2 
      3 #[cfg(feature = "serde_json")]
      4 pub mod decode;
      5 
      6 #[cfg(all(test, feature = "serde_json"))]
      7 mod tests {
      8     use radroots_events::{
      9         farm::RadrootsFarmRef,
     10         farm_crdt::KIND_FARM_CRDT_CHANGE,
     11         farm_workspace::{
     12             KIND_FARM_WORKSPACE_MANIFEST, RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION,
     13             RADROOTS_FARM_WORKSPACE_SCHEMA, RADROOTS_FARM_WORKSPACE_TAG,
     14             RadrootsFarmWorkspaceManifest, RadrootsFarmWorkspaceMediaServer,
     15             RadrootsFarmWorkspaceRelay, RadrootsFarmWorkspaceRelayMode,
     16         },
     17         kinds::{KIND_FARM, KIND_FARM_FILE_METADATA, KIND_POST},
     18     };
     19 
     20     use crate::error::{EventEncodeError, EventParseError};
     21     use crate::farm_workspace::decode::{
     22         data_from_event, farm_workspace_from_event, parsed_from_event,
     23     };
     24     use crate::farm_workspace::encode::{
     25         farm_workspace_build_tags, to_wire_parts, to_wire_parts_with_kind,
     26     };
     27 
     28     const D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
     29     const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ";
     30     const GROUP_ID: &str = "field-group";
     31 
     32     #[test]
     33     fn farm_workspace_manifest_encodes_canonical_tags_and_decodes() {
     34         let manifest = sample_manifest();
     35         let parts = to_wire_parts(&manifest).expect("workspace wire parts");
     36 
     37         assert_eq!(parts.kind, KIND_FARM_WORKSPACE_MANIFEST);
     38         assert!(parts.tags.contains(&tag("d", D_TAG)));
     39         assert!(parts.tags.contains(&tag("h", GROUP_ID)));
     40         assert!(parts.tags.contains(&tag("p", "workspace_owner_pubkey")));
     41         assert!(parts.tags.contains(&tag("t", RADROOTS_FARM_WORKSPACE_TAG)));
     42         assert!(
     43             parts
     44                 .tags
     45                 .contains(&tag("a", "30340:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAQ"))
     46         );
     47 
     48         let decoded = farm_workspace_from_event(parts.kind, &parts.tags, &parts.content)
     49             .expect("workspace decode");
     50         assert_eq!(decoded.d_tag, D_TAG);
     51         assert_eq!(decoded.schema, RADROOTS_FARM_WORKSPACE_SCHEMA);
     52         assert_eq!(decoded.farm_group_id, GROUP_ID);
     53         assert_eq!(
     54             decoded.supported_kinds,
     55             vec![
     56                 KIND_FARM_CRDT_CHANGE,
     57                 KIND_FARM_WORKSPACE_MANIFEST,
     58                 KIND_FARM_FILE_METADATA
     59             ]
     60         );
     61     }
     62 
     63     #[test]
     64     fn farm_workspace_manifest_rejects_missing_h_and_d_mismatch() {
     65         let parts = to_wire_parts(&sample_manifest()).expect("workspace wire parts");
     66         let without_h = parts
     67             .tags
     68             .iter()
     69             .filter(|tag| tag.first().map(|value| value.as_str()) != Some("h"))
     70             .cloned()
     71             .collect::<Vec<_>>();
     72 
     73         let missing_h =
     74             farm_workspace_from_event(parts.kind, &without_h, &parts.content).unwrap_err();
     75         assert!(matches!(missing_h, EventParseError::MissingTag("h")));
     76 
     77         let mut mismatched_d = parts.tags.clone();
     78         for tag in mismatched_d.iter_mut() {
     79             if tag.first().map(|value| value.as_str()) == Some("d") {
     80                 tag[1] = "AAAAAAAAAAAAAAAAAAAAAg".to_string();
     81             }
     82         }
     83         let mismatch =
     84             farm_workspace_from_event(parts.kind, &mismatched_d, &parts.content).unwrap_err();
     85         assert!(matches!(mismatch, EventParseError::InvalidTag("d")));
     86     }
     87 
     88     #[test]
     89     fn farm_workspace_manifest_rejects_bad_d_tag_kind_and_schema() {
     90         let mut bad_d_tag = sample_manifest();
     91         bad_d_tag.d_tag = "bad".to_string();
     92         let encode_err = farm_workspace_build_tags(&bad_d_tag).unwrap_err();
     93         assert!(matches!(
     94             encode_err,
     95             EventEncodeError::InvalidField("d_tag")
     96         ));
     97 
     98         let wrong_kind = to_wire_parts_with_kind(&sample_manifest(), KIND_POST).unwrap_err();
     99         assert!(matches!(
    100             wrong_kind,
    101             EventEncodeError::InvalidKind(KIND_POST)
    102         ));
    103 
    104         let mut bad_schema = sample_manifest();
    105         bad_schema.schema = "radroots.farm.workspace.invalid".to_string();
    106         let content = serde_json::to_string(&bad_schema).expect("bad schema content");
    107         let tags = farm_workspace_build_tags(&sample_manifest()).expect("workspace tags");
    108         let schema_err =
    109             farm_workspace_from_event(KIND_FARM_WORKSPACE_MANIFEST, &tags, &content).unwrap_err();
    110         assert!(matches!(schema_err, EventParseError::InvalidJson("schema")));
    111     }
    112 
    113     #[test]
    114     fn farm_workspace_manifest_rejects_missing_field_usage_kinds_and_relays() {
    115         let mut no_relays = sample_manifest();
    116         no_relays.relays.clear();
    117         let relay_err = to_wire_parts(&no_relays).unwrap_err();
    118         assert!(matches!(
    119             relay_err,
    120             EventEncodeError::EmptyRequiredField("relays")
    121         ));
    122 
    123         let mut unsupported = sample_manifest();
    124         unsupported.supported_kinds = vec![KIND_FARM_WORKSPACE_MANIFEST];
    125         let supported_err = to_wire_parts(&unsupported).unwrap_err();
    126         assert!(matches!(
    127             supported_err,
    128             EventEncodeError::InvalidField("supported_kinds")
    129         ));
    130     }
    131 
    132     #[test]
    133     fn farm_workspace_manifest_requires_farm_file_support_for_media_servers() {
    134         let mut no_file_support = sample_manifest();
    135         no_file_support.supported_kinds = vec![KIND_FARM_CRDT_CHANGE, KIND_FARM_WORKSPACE_MANIFEST];
    136         let encode_err = to_wire_parts(&no_file_support).unwrap_err();
    137         assert!(matches!(
    138             encode_err,
    139             EventEncodeError::InvalidField("supported_kinds")
    140         ));
    141 
    142         let mut no_media = no_file_support.clone();
    143         no_media.media_servers.clear();
    144         let parts = to_wire_parts(&no_media).expect("non-media manifest remains valid");
    145         let decoded = farm_workspace_from_event(parts.kind, &parts.tags, &parts.content)
    146             .expect("non-media manifest decodes");
    147         assert!(decoded.media_servers.is_empty());
    148 
    149         let mut content_missing_file_support = sample_manifest();
    150         content_missing_file_support.supported_kinds =
    151             vec![KIND_FARM_CRDT_CHANGE, KIND_FARM_WORKSPACE_MANIFEST];
    152         let content =
    153             serde_json::to_string(&content_missing_file_support).expect("workspace content");
    154         let tags = farm_workspace_build_tags(&sample_manifest()).expect("workspace tags");
    155         let parse_err =
    156             farm_workspace_from_event(KIND_FARM_WORKSPACE_MANIFEST, &tags, &content).unwrap_err();
    157         assert!(matches!(
    158             parse_err,
    159             EventParseError::InvalidJson("supported_kinds")
    160         ));
    161     }
    162 
    163     #[test]
    164     fn farm_workspace_manifest_wrappers_roundtrip_optional_farm() {
    165         let mut manifest = sample_manifest();
    166         manifest.farm = None;
    167         manifest.media_servers.clear();
    168         let parts = to_wire_parts(&manifest).expect("workspace wire parts");
    169         assert!(
    170             !parts
    171                 .tags
    172                 .iter()
    173                 .any(|tag| tag.first().map(String::as_str) == Some("a"))
    174         );
    175 
    176         let data = data_from_event(
    177             "event-id".to_string(),
    178             "author-pubkey".to_string(),
    179             99,
    180             parts.kind,
    181             parts.content.clone(),
    182             parts.tags.clone(),
    183         )
    184         .expect("parsed data");
    185         assert_eq!(data.id, "event-id");
    186         assert_eq!(data.author, "author-pubkey");
    187         assert_eq!(data.published_at, 99);
    188         assert_eq!(data.kind, KIND_FARM_WORKSPACE_MANIFEST);
    189         assert_same_manifest(&data.data, &manifest);
    190 
    191         let parsed = parsed_from_event(
    192             "event-id".to_string(),
    193             "author-pubkey".to_string(),
    194             99,
    195             parts.kind,
    196             parts.content,
    197             parts.tags,
    198             "sig".to_string(),
    199         )
    200         .expect("parsed event");
    201         assert_eq!(parsed.event.sig, "sig");
    202         assert_same_manifest(&parsed.data.data, &manifest);
    203     }
    204 
    205     #[test]
    206     fn farm_workspace_manifest_rejects_decode_tag_and_content_edges() {
    207         let parts = to_wire_parts(&sample_manifest()).expect("workspace wire parts");
    208 
    209         let wrong_kind =
    210             farm_workspace_from_event(KIND_POST, &parts.tags, &parts.content).unwrap_err();
    211         assert!(matches!(
    212             wrong_kind,
    213             EventParseError::InvalidKind {
    214                 expected: "30078",
    215                 got: KIND_POST
    216             }
    217         ));
    218 
    219         let empty_content = farm_workspace_from_event(parts.kind, &parts.tags, " ").unwrap_err();
    220         assert!(matches!(
    221             empty_content,
    222             EventParseError::InvalidJson("content")
    223         ));
    224 
    225         let bad_json = farm_workspace_from_event(parts.kind, &parts.tags, "{").unwrap_err();
    226         assert!(matches!(bad_json, EventParseError::InvalidJson("content")));
    227 
    228         let mut owner_mismatch = parts.tags.clone();
    229         replace_first_tag(&mut owner_mismatch, "p", tag("p", "other_owner"));
    230         let err =
    231             farm_workspace_from_event(parts.kind, &owner_mismatch, &parts.content).unwrap_err();
    232         assert!(matches!(err, EventParseError::InvalidTag("p")));
    233 
    234         let mut missing_marker = parts.tags.clone();
    235         remove_tags(&mut missing_marker, "t");
    236         let err =
    237             farm_workspace_from_event(parts.kind, &missing_marker, &parts.content).unwrap_err();
    238         assert!(matches!(err, EventParseError::MissingTag("t")));
    239 
    240         for replacement in [
    241             tag(
    242                 "a",
    243                 &format!("{KIND_FARM}:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAg"),
    244             ),
    245             tag("a", "30023:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAQ"),
    246             tag("a", &format!("{KIND_FARM}:farm_pubkey:bad d")),
    247         ] {
    248             let mut tags = parts.tags.clone();
    249             replace_first_tag(&mut tags, "a", replacement);
    250             let err = farm_workspace_from_event(parts.kind, &tags, &parts.content).unwrap_err();
    251             assert!(matches!(err, EventParseError::InvalidTag("a")));
    252         }
    253     }
    254 
    255     #[test]
    256     fn farm_workspace_manifest_rejects_content_validation_edges() {
    257         let parts = to_wire_parts(&sample_manifest()).expect("workspace wire parts");
    258 
    259         for (manifest, expected) in [
    260             {
    261                 let mut manifest = sample_manifest();
    262                 manifest.farm_group_id.clear();
    263                 (manifest, EventParseError::InvalidTag("h"))
    264             },
    265             {
    266                 let mut manifest = sample_manifest();
    267                 manifest.owner_pubkey.clear();
    268                 (manifest, EventParseError::InvalidTag("p"))
    269             },
    270             {
    271                 let mut manifest = sample_manifest();
    272                 manifest.relays.clear();
    273                 (manifest, EventParseError::InvalidJson("relays"))
    274             },
    275             {
    276                 let mut manifest = sample_manifest();
    277                 manifest.supported_kinds = vec![KIND_FARM_WORKSPACE_MANIFEST];
    278                 (manifest, EventParseError::InvalidJson("supported_kinds"))
    279             },
    280             {
    281                 let mut manifest = sample_manifest();
    282                 manifest.name.clear();
    283                 (manifest, EventParseError::InvalidJson("name"))
    284             },
    285             {
    286                 let mut manifest = sample_manifest();
    287                 manifest.protocol_version.clear();
    288                 (manifest, EventParseError::InvalidJson("protocol_version"))
    289             },
    290             {
    291                 let mut manifest = sample_manifest();
    292                 manifest.relays[0].url.clear();
    293                 (manifest, EventParseError::InvalidJson("relays.url"))
    294             },
    295             {
    296                 let mut manifest = sample_manifest();
    297                 manifest.media_servers[0].service.clear();
    298                 (
    299                     manifest,
    300                     EventParseError::InvalidJson("media_servers.service"),
    301                 )
    302             },
    303             {
    304                 let mut manifest = sample_manifest();
    305                 manifest.farm.as_mut().unwrap().d_tag = "bad d".to_string();
    306                 (manifest, EventParseError::InvalidTag("d"))
    307             },
    308         ] {
    309             let content = serde_json::to_string(&manifest).expect("workspace content");
    310             let err = farm_workspace_from_event(parts.kind, &parts.tags, &content).unwrap_err();
    311             assert_same_parse_error(err, expected);
    312         }
    313     }
    314 
    315     #[test]
    316     fn farm_workspace_manifest_rejects_encoder_validation_edges() {
    317         for (manifest, expected) in [
    318             {
    319                 let mut manifest = sample_manifest();
    320                 manifest.name.clear();
    321                 (manifest, EventEncodeError::EmptyRequiredField("name"))
    322             },
    323             {
    324                 let mut manifest = sample_manifest();
    325                 manifest.owner_pubkey.clear();
    326                 (
    327                     manifest,
    328                     EventEncodeError::EmptyRequiredField("owner_pubkey"),
    329                 )
    330             },
    331             {
    332                 let mut manifest = sample_manifest();
    333                 manifest.protocol_version.clear();
    334                 (
    335                     manifest,
    336                     EventEncodeError::EmptyRequiredField("protocol_version"),
    337                 )
    338             },
    339             {
    340                 let mut manifest = sample_manifest();
    341                 manifest.relays[0].url.clear();
    342                 (manifest, EventEncodeError::EmptyRequiredField("relays.url"))
    343             },
    344             {
    345                 let mut manifest = sample_manifest();
    346                 manifest.media_servers[0].url.clear();
    347                 (
    348                     manifest,
    349                     EventEncodeError::EmptyRequiredField("media_servers.url"),
    350                 )
    351             },
    352             {
    353                 let mut manifest = sample_manifest();
    354                 manifest.media_servers[0].service.clear();
    355                 (
    356                     manifest,
    357                     EventEncodeError::EmptyRequiredField("media_servers.service"),
    358                 )
    359             },
    360             {
    361                 let mut manifest = sample_manifest();
    362                 manifest.farm.as_mut().unwrap().pubkey.clear();
    363                 (
    364                     manifest,
    365                     EventEncodeError::EmptyRequiredField("farm.pubkey"),
    366                 )
    367             },
    368             {
    369                 let mut manifest = sample_manifest();
    370                 manifest.farm.as_mut().unwrap().d_tag = "bad d".to_string();
    371                 (manifest, EventEncodeError::InvalidField("farm.d_tag"))
    372             },
    373         ] {
    374             let err = farm_workspace_build_tags(&manifest).unwrap_err();
    375             assert_same_encode_error(err, expected);
    376         }
    377     }
    378 
    379     fn sample_manifest() -> RadrootsFarmWorkspaceManifest {
    380         RadrootsFarmWorkspaceManifest {
    381             d_tag: D_TAG.to_string(),
    382             schema: RADROOTS_FARM_WORKSPACE_SCHEMA.to_string(),
    383             farm_group_id: GROUP_ID.to_string(),
    384             name: "Small Regen Farm".to_string(),
    385             owner_pubkey: "workspace_owner_pubkey".to_string(),
    386             farm: Some(RadrootsFarmRef {
    387                 pubkey: "farm_pubkey".to_string(),
    388                 d_tag: FARM_D_TAG.to_string(),
    389             }),
    390             relays: vec![RadrootsFarmWorkspaceRelay {
    391                 url: "wss://relay.example.invalid/farm/field-group".to_string(),
    392                 mode: RadrootsFarmWorkspaceRelayMode::ReadWrite,
    393             }],
    394             media_servers: vec![RadrootsFarmWorkspaceMediaServer {
    395                 url: "https://media.example.invalid/farm/field-group".to_string(),
    396                 service: "RadrootsPrivateMedia".to_string(),
    397             }],
    398             supported_kinds: vec![
    399                 KIND_FARM_CRDT_CHANGE,
    400                 KIND_FARM_WORKSPACE_MANIFEST,
    401                 KIND_FARM_FILE_METADATA,
    402             ],
    403             protocol_version: RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION.to_string(),
    404             created_at_ms: 1_780_000_000_000,
    405             updated_at_ms: None,
    406         }
    407     }
    408 
    409     fn tag(key: &str, value: &str) -> Vec<String> {
    410         vec![key.to_string(), value.to_string()]
    411     }
    412 
    413     fn remove_tags(tags: &mut Vec<Vec<String>>, name: &str) {
    414         tags.retain(|tag| tag.first().map(String::as_str) != Some(name));
    415     }
    416 
    417     fn replace_first_tag(tags: &mut [Vec<String>], name: &str, replacement: Vec<String>) {
    418         let tag = tags
    419             .iter_mut()
    420             .find(|tag| tag.first().map(String::as_str) == Some(name))
    421             .expect("tag");
    422         *tag = replacement;
    423     }
    424 
    425     fn assert_same_manifest(
    426         actual: &RadrootsFarmWorkspaceManifest,
    427         expected: &RadrootsFarmWorkspaceManifest,
    428     ) {
    429         assert_eq!(
    430             serde_json::to_value(actual).expect("actual manifest value"),
    431             serde_json::to_value(expected).expect("expected manifest value")
    432         );
    433     }
    434 
    435     fn assert_same_parse_error(actual: EventParseError, expected: EventParseError) {
    436         match (actual, expected) {
    437             (EventParseError::MissingTag(actual), EventParseError::MissingTag(expected))
    438             | (EventParseError::InvalidTag(actual), EventParseError::InvalidTag(expected))
    439             | (EventParseError::InvalidJson(actual), EventParseError::InvalidJson(expected)) => {
    440                 assert_eq!(actual, expected);
    441             }
    442             (
    443                 EventParseError::InvalidKind {
    444                     expected: actual_expected,
    445                     got: actual_got,
    446                 },
    447                 EventParseError::InvalidKind { expected, got },
    448             ) => {
    449                 assert_eq!(actual_expected, expected);
    450                 assert_eq!(actual_got, got);
    451             }
    452             (
    453                 EventParseError::InvalidNumber(actual, _),
    454                 EventParseError::InvalidNumber(expected, _),
    455             ) => {
    456                 assert_eq!(actual, expected);
    457             }
    458             (actual, expected) => {
    459                 panic!("unexpected parse error {actual:?}, expected {expected:?}")
    460             }
    461         }
    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) => {
    478                 panic!("unexpected encode error {actual:?}, expected {expected:?}")
    479             }
    480         }
    481     }
    482 }