lib

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

mod.rs (19123B)


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