lib

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

mod.rs (25396B)


      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_crdt::{
     10             KIND_FARM_CRDT_CHANGE, RADROOTS_FARM_CRDT_CHANGE_SCHEMA, RADROOTS_FARM_CRDT_TAG,
     11             RadrootsCrdtBackend, RadrootsFarmCrdtChange, RadrootsFarmCrdtDocumentKind,
     12             RadrootsFarmSemanticKind,
     13         },
     14         farm_workspace::{KIND_FARM_WORKSPACE_MANIFEST, RadrootsFarmWorkspaceRef},
     15         kinds::KIND_POST,
     16     };
     17 
     18     use crate::error::{EventEncodeError, EventParseError};
     19     use crate::farm_crdt::decode::{
     20         data_from_event, farm_crdt_change_from_event, farm_crdt_change_from_event_with_author,
     21         parsed_from_event,
     22     };
     23     use crate::farm_crdt::encode::{
     24         farm_crdt_change_build_tags, farm_crdt_change_build_tags_with_author, to_wire_parts,
     25         to_wire_parts_with_author, to_wire_parts_with_kind, to_wire_parts_with_kind_and_author,
     26     };
     27 
     28     const WORKSPACE_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
     29     const DOCUMENT_ID: &str = "AAAAAAAAAAAAAAAAAAAAAg";
     30     const GROUP_ID: &str = "field-group";
     31     const AUTHOR: &str = "author_pubkey";
     32 
     33     #[test]
     34     fn farm_crdt_change_encodes_and_decodes_task_change() {
     35         let change = sample_change();
     36         let parts = to_wire_parts_with_author(&change, AUTHOR).expect("crdt wire parts");
     37 
     38         assert_eq!(parts.kind, KIND_FARM_CRDT_CHANGE);
     39         assert!(parts.tags.contains(&tag("h", GROUP_ID)));
     40         assert!(parts.tags.contains(&tag("d", DOCUMENT_ID)));
     41         assert!(
     42             parts
     43                 .tags
     44                 .contains(&tag("a", "30078:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA"))
     45         );
     46         assert!(parts.tags.contains(&tag("p", AUTHOR)));
     47         assert!(parts.tags.contains(&tag("t", RADROOTS_FARM_CRDT_TAG)));
     48 
     49         let decoded = farm_crdt_change_from_event_with_author(
     50             parts.kind,
     51             &parts.tags,
     52             &parts.content,
     53             AUTHOR,
     54         )
     55         .expect("crdt decode");
     56         assert_eq!(decoded.schema, RADROOTS_FARM_CRDT_CHANGE_SCHEMA);
     57         assert_eq!(decoded.document_id, DOCUMENT_ID);
     58         assert_eq!(decoded.workspace.d_tag, WORKSPACE_D_TAG);
     59         assert_eq!(decoded.business_time_ms, 1_780_000_000_000);
     60     }
     61 
     62     #[test]
     63     fn farm_crdt_change_roundtrips_representative_mvp_semantics() {
     64         let cases = vec![
     65             (
     66                 RadrootsFarmCrdtDocumentKind::FarmTask,
     67                 RadrootsFarmSemanticKind::FarmTaskCreate,
     68             ),
     69             (
     70                 RadrootsFarmCrdtDocumentKind::FarmTask,
     71                 RadrootsFarmSemanticKind::FarmTaskStatusSet,
     72             ),
     73             (
     74                 RadrootsFarmCrdtDocumentKind::FarmWorkSession,
     75                 RadrootsFarmSemanticKind::FarmWorkSessionStart,
     76             ),
     77             (
     78                 RadrootsFarmCrdtDocumentKind::FarmWorkSession,
     79                 RadrootsFarmSemanticKind::FarmWorkSessionStop,
     80             ),
     81             (
     82                 RadrootsFarmCrdtDocumentKind::FarmWorkSession,
     83                 RadrootsFarmSemanticKind::FarmWorkSessionSubmit,
     84             ),
     85             (
     86                 RadrootsFarmCrdtDocumentKind::FarmWorkSession,
     87                 RadrootsFarmSemanticKind::FarmWorkSessionApprove,
     88             ),
     89             (
     90                 RadrootsFarmCrdtDocumentKind::FarmWorkSession,
     91                 RadrootsFarmSemanticKind::FarmWorkSessionReject,
     92             ),
     93             (
     94                 RadrootsFarmCrdtDocumentKind::FarmWorkSession,
     95                 RadrootsFarmSemanticKind::FarmWorkSessionCorrect,
     96             ),
     97             (
     98                 RadrootsFarmCrdtDocumentKind::FarmHarvestRecord,
     99                 RadrootsFarmSemanticKind::FarmHarvestLineAdd,
    100             ),
    101             (
    102                 RadrootsFarmCrdtDocumentKind::FarmHarvestRecord,
    103                 RadrootsFarmSemanticKind::FarmHarvestLineCorrect,
    104             ),
    105             (
    106                 RadrootsFarmCrdtDocumentKind::FarmHarvestRecord,
    107                 RadrootsFarmSemanticKind::FarmHarvestLineVoid,
    108             ),
    109             (
    110                 RadrootsFarmCrdtDocumentKind::FarmMembership,
    111                 RadrootsFarmSemanticKind::FarmMemberInviteCreate,
    112             ),
    113             (
    114                 RadrootsFarmCrdtDocumentKind::FarmMembership,
    115                 RadrootsFarmSemanticKind::FarmMemberApprove,
    116             ),
    117             (
    118                 RadrootsFarmCrdtDocumentKind::FarmMembership,
    119                 RadrootsFarmSemanticKind::FarmMemberRoleSet,
    120             ),
    121             (
    122                 RadrootsFarmCrdtDocumentKind::FarmPayPeriod,
    123                 RadrootsFarmSemanticKind::FarmPayPeriodClose,
    124             ),
    125             (
    126                 RadrootsFarmCrdtDocumentKind::FarmPayPeriod,
    127                 RadrootsFarmSemanticKind::FarmReportExportMark,
    128             ),
    129         ];
    130 
    131         for (index, (document_kind, semantic_kind)) in cases.into_iter().enumerate() {
    132             let document_id = document_id(index);
    133             let change = sample_change_with(document_id.as_str(), document_kind, semantic_kind);
    134             let parts = to_wire_parts_with_author(&change, AUTHOR).expect("crdt wire parts");
    135             let decoded = farm_crdt_change_from_event_with_author(
    136                 parts.kind,
    137                 &parts.tags,
    138                 &parts.content,
    139                 AUTHOR,
    140             )
    141             .expect("crdt decode");
    142 
    143             assert_eq!(decoded.document_id, document_id);
    144             assert_eq!(decoded.document_kind, change.document_kind);
    145             assert_eq!(decoded.semantic_kind, change.semantic_kind);
    146         }
    147     }
    148 
    149     #[test]
    150     fn farm_crdt_change_rejects_missing_t_and_d_mismatch() {
    151         let parts = to_wire_parts(&sample_change()).expect("crdt wire parts");
    152         let without_t = parts
    153             .tags
    154             .iter()
    155             .filter(|tag| tag.first().map(|value| value.as_str()) != Some("t"))
    156             .cloned()
    157             .collect::<Vec<_>>();
    158 
    159         let missing_t =
    160             farm_crdt_change_from_event(parts.kind, &without_t, &parts.content).unwrap_err();
    161         assert!(matches!(missing_t, EventParseError::MissingTag("t")));
    162 
    163         let mut mismatched_d = parts.tags.clone();
    164         for tag in mismatched_d.iter_mut() {
    165             if tag.first().map(|value| value.as_str()) == Some("d") {
    166                 tag[1] = WORKSPACE_D_TAG.to_string();
    167             }
    168         }
    169         let mismatch =
    170             farm_crdt_change_from_event(parts.kind, &mismatched_d, &parts.content).unwrap_err();
    171         assert!(matches!(mismatch, EventParseError::InvalidTag("d")));
    172     }
    173 
    174     #[test]
    175     fn farm_crdt_change_rejects_bad_workspace_address_and_author() {
    176         let parts = to_wire_parts_with_author(&sample_change(), AUTHOR).expect("crdt wire parts");
    177         let mut bad_workspace = parts.tags.clone();
    178         for tag in bad_workspace.iter_mut() {
    179             if tag.first().map(|value| value.as_str()) == Some("a") {
    180                 tag[1] = format!("{KIND_FARM_WORKSPACE_MANIFEST}:workspace_pubkey:bad");
    181             }
    182         }
    183         let workspace_err =
    184             farm_crdt_change_from_event(parts.kind, &bad_workspace, &parts.content).unwrap_err();
    185         assert!(matches!(workspace_err, EventParseError::InvalidTag("a")));
    186 
    187         let author_err = farm_crdt_change_from_event_with_author(
    188             parts.kind,
    189             &parts.tags,
    190             &parts.content,
    191             "other_author",
    192         )
    193         .unwrap_err();
    194         assert!(matches!(author_err, EventParseError::InvalidTag("p")));
    195     }
    196 
    197     #[test]
    198     fn farm_crdt_change_rejects_bad_encoded_change_missing_h_and_kind() {
    199         let mut bad_change = sample_change();
    200         bad_change.encoded_change = "abc/def".to_string();
    201         let encode_err = farm_crdt_change_build_tags(&bad_change).unwrap_err();
    202         assert!(matches!(
    203             encode_err,
    204             EventEncodeError::InvalidField("encoded_change")
    205         ));
    206 
    207         let parts = to_wire_parts(&sample_change()).expect("crdt wire parts");
    208         let without_h = parts
    209             .tags
    210             .iter()
    211             .filter(|tag| tag.first().map(|value| value.as_str()) != Some("h"))
    212             .cloned()
    213             .collect::<Vec<_>>();
    214         let missing_h =
    215             farm_crdt_change_from_event(parts.kind, &without_h, &parts.content).unwrap_err();
    216         assert!(matches!(missing_h, EventParseError::MissingTag("h")));
    217 
    218         let wrong_kind = to_wire_parts_with_kind(&sample_change(), KIND_POST).unwrap_err();
    219         assert!(matches!(
    220             wrong_kind,
    221             EventEncodeError::InvalidKind(KIND_POST)
    222         ));
    223 
    224         let decode_wrong_kind =
    225             farm_crdt_change_from_event(KIND_POST, &parts.tags, &parts.content).unwrap_err();
    226         assert!(matches!(
    227             decode_wrong_kind,
    228             EventParseError::InvalidKind {
    229                 expected: "78",
    230                 got: KIND_POST
    231             }
    232         ));
    233     }
    234 
    235     #[test]
    236     fn farm_crdt_change_rejects_zero_business_time_and_schema_mismatch() {
    237         let mut zero_time = sample_change();
    238         zero_time.business_time_ms = 0;
    239         let zero_err = to_wire_parts(&zero_time).unwrap_err();
    240         assert!(matches!(
    241             zero_err,
    242             EventEncodeError::InvalidField("business_time_ms")
    243         ));
    244 
    245         let parts = to_wire_parts(&sample_change()).expect("crdt wire parts");
    246         let mut bad_schema = sample_change();
    247         bad_schema.schema = "radroots.farm.crdt.invalid".to_string();
    248         let content = serde_json::to_string(&bad_schema).expect("bad schema content");
    249         let schema_err =
    250             farm_crdt_change_from_event(parts.kind, &parts.tags, &content).unwrap_err();
    251         assert!(matches!(schema_err, EventParseError::InvalidJson("schema")));
    252     }
    253 
    254     #[test]
    255     fn farm_crdt_change_wrappers_preserve_event_metadata() {
    256         let change = sample_change();
    257         let parts = to_wire_parts_with_author(&change, AUTHOR).expect("crdt wire parts");
    258 
    259         let data = data_from_event(
    260             "event-id".to_string(),
    261             AUTHOR.to_string(),
    262             99,
    263             parts.kind,
    264             parts.content.clone(),
    265             parts.tags.clone(),
    266         )
    267         .expect("parsed data");
    268         assert_eq!(data.id, "event-id");
    269         assert_eq!(data.author, AUTHOR);
    270         assert_eq!(data.published_at, 99);
    271         assert_eq!(data.kind, KIND_FARM_CRDT_CHANGE);
    272         assert_eq!(data.data, change);
    273 
    274         let parsed = parsed_from_event(
    275             "event-id".to_string(),
    276             AUTHOR.to_string(),
    277             99,
    278             parts.kind,
    279             parts.content,
    280             parts.tags,
    281             "sig".to_string(),
    282         )
    283         .expect("parsed event");
    284         assert_eq!(parsed.event.sig, "sig");
    285         assert_eq!(parsed.data.data, change);
    286 
    287         let no_author_parts = to_wire_parts(&change).expect("crdt wire parts");
    288         let decoded = farm_crdt_change_from_event_with_author(
    289             no_author_parts.kind,
    290             &no_author_parts.tags,
    291             &no_author_parts.content,
    292             AUTHOR,
    293         )
    294         .expect("author context without p tag remains valid");
    295         assert_eq!(decoded, change);
    296 
    297         let empty_author = farm_crdt_change_from_event_with_author(
    298             no_author_parts.kind,
    299             &no_author_parts.tags,
    300             &no_author_parts.content,
    301             " ",
    302         )
    303         .unwrap_err();
    304         assert!(matches!(empty_author, EventParseError::InvalidTag("p")));
    305     }
    306 
    307     #[test]
    308     fn farm_crdt_change_rejects_decode_tag_and_content_edges() {
    309         let parts = to_wire_parts_with_author(&sample_change(), AUTHOR).expect("crdt wire parts");
    310 
    311         let empty_content = farm_crdt_change_from_event(parts.kind, &parts.tags, " ").unwrap_err();
    312         assert!(matches!(
    313             empty_content,
    314             EventParseError::InvalidJson("content")
    315         ));
    316 
    317         let bad_json = farm_crdt_change_from_event(parts.kind, &parts.tags, "{").unwrap_err();
    318         assert!(matches!(bad_json, EventParseError::InvalidJson("content")));
    319 
    320         let mut empty_author_tag = parts.tags.clone();
    321         replace_first_tag(&mut empty_author_tag, "p", tag("p", " "));
    322         let err = farm_crdt_change_from_event_with_author(
    323             parts.kind,
    324             &empty_author_tag,
    325             &parts.content,
    326             AUTHOR,
    327         )
    328         .unwrap_err();
    329         assert!(matches!(err, EventParseError::InvalidTag("p")));
    330 
    331         let mut bad_document_tag = parts.tags.clone();
    332         replace_first_tag(&mut bad_document_tag, "d", tag("d", "bad"));
    333         let err =
    334             farm_crdt_change_from_event(parts.kind, &bad_document_tag, &parts.content).unwrap_err();
    335         assert!(matches!(err, EventParseError::InvalidTag("d")));
    336 
    337         for replacement in [
    338             tag("a", "30023:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA"),
    339             tag("a", "30078::AAAAAAAAAAAAAAAAAAAAAA"),
    340             tag("a", "30078:workspace_pubkey:bad d"),
    341             tag("a", "30078:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA:extra"),
    342         ] {
    343             let mut tags = parts.tags.clone();
    344             replace_first_tag(&mut tags, "a", replacement);
    345             let err = farm_crdt_change_from_event(parts.kind, &tags, &parts.content).unwrap_err();
    346             assert!(matches!(err, EventParseError::InvalidTag("a")));
    347         }
    348 
    349         let mut wrong_marker = parts.tags.clone();
    350         remove_tags(&mut wrong_marker, "t");
    351         wrong_marker.push(tag("t", "radroots:farm:other"));
    352         let err =
    353             farm_crdt_change_from_event(parts.kind, &wrong_marker, &parts.content).unwrap_err();
    354         assert!(matches!(err, EventParseError::MissingTag("t")));
    355 
    356         let mut group_mismatch = parts.tags.clone();
    357         replace_first_tag(&mut group_mismatch, "h", tag("h", "other-group"));
    358         let err =
    359             farm_crdt_change_from_event(parts.kind, &group_mismatch, &parts.content).unwrap_err();
    360         assert!(matches!(err, EventParseError::InvalidTag("h")));
    361 
    362         let mut pubkey_mismatch = sample_change();
    363         pubkey_mismatch.workspace.pubkey = "other_workspace_pubkey".to_string();
    364         let content = serde_json::to_string(&pubkey_mismatch).expect("crdt content");
    365         let err = farm_crdt_change_from_event(parts.kind, &parts.tags, &content).unwrap_err();
    366         assert!(matches!(err, EventParseError::InvalidTag("a")));
    367 
    368         let mut d_tag_mismatch = sample_change();
    369         d_tag_mismatch.workspace.d_tag = DOCUMENT_ID.to_string();
    370         let content = serde_json::to_string(&d_tag_mismatch).expect("crdt content");
    371         let err = farm_crdt_change_from_event(parts.kind, &parts.tags, &content).unwrap_err();
    372         assert!(matches!(err, EventParseError::InvalidTag("a")));
    373     }
    374 
    375     #[test]
    376     fn farm_crdt_change_rejects_content_validation_edges() {
    377         let parts = to_wire_parts(&sample_change()).expect("crdt wire parts");
    378 
    379         for (change, expected) in [
    380             {
    381                 let mut change = sample_change();
    382                 change.farm_group_id.clear();
    383                 (change, EventParseError::InvalidTag("h"))
    384             },
    385             {
    386                 let mut change = sample_change();
    387                 change.document_id = "bad".to_string();
    388                 (change, EventParseError::InvalidTag("d"))
    389             },
    390             {
    391                 let mut change = sample_change();
    392                 change.workspace.pubkey.clear();
    393                 (change, EventParseError::InvalidTag("a"))
    394             },
    395             {
    396                 let mut change = sample_change();
    397                 change.workspace.d_tag = "bad".to_string();
    398                 (change, EventParseError::InvalidTag("a"))
    399             },
    400             {
    401                 let mut change = sample_change();
    402                 change.encoded_change = "abc/def".to_string();
    403                 (change, EventParseError::InvalidJson("encoded_change"))
    404             },
    405             {
    406                 let mut change = sample_change();
    407                 change.change_hash.clear();
    408                 (change, EventParseError::InvalidJson("change_hash"))
    409             },
    410             {
    411                 let mut change = sample_change();
    412                 change.business_time_ms = 0;
    413                 (change, EventParseError::InvalidJson("business_time_ms"))
    414             },
    415             {
    416                 let mut change = sample_change();
    417                 change.actor_id.clear();
    418                 (change, EventParseError::InvalidJson("actor_id"))
    419             },
    420             {
    421                 let mut change = sample_change();
    422                 change.dependencies.push(String::new());
    423                 (change, EventParseError::InvalidJson("dependencies"))
    424             },
    425             {
    426                 let mut change = sample_change();
    427                 change.crdt_backend_version = Some(" ".to_string());
    428                 (change, EventParseError::InvalidJson("crdt_backend_version"))
    429             },
    430             {
    431                 let mut change = sample_change();
    432                 change.author_member_id = Some(" ".to_string());
    433                 (change, EventParseError::InvalidJson("author_member_id"))
    434             },
    435             {
    436                 let mut change = sample_change();
    437                 change.app_version = Some(" ".to_string());
    438                 (change, EventParseError::InvalidJson("app_version"))
    439             },
    440         ] {
    441             let content = serde_json::to_string(&change).expect("crdt content");
    442             let err = farm_crdt_change_from_event(parts.kind, &parts.tags, &content).unwrap_err();
    443             assert_same_parse_error(err, expected);
    444         }
    445     }
    446 
    447     #[test]
    448     fn farm_crdt_change_rejects_encoder_validation_edges() {
    449         for (change, expected) in [
    450             {
    451                 let mut change = sample_change();
    452                 change.schema = "radroots.farm.crdt.invalid".to_string();
    453                 (change, EventEncodeError::InvalidField("schema"))
    454             },
    455             {
    456                 let mut change = sample_change();
    457                 change.farm_group_id.clear();
    458                 (
    459                     change,
    460                     EventEncodeError::EmptyRequiredField("farm_group_id"),
    461                 )
    462             },
    463             {
    464                 let mut change = sample_change();
    465                 change.document_id = "bad".to_string();
    466                 (change, EventEncodeError::InvalidField("document_id"))
    467             },
    468             {
    469                 let mut change = sample_change();
    470                 change.workspace.pubkey.clear();
    471                 (
    472                     change,
    473                     EventEncodeError::EmptyRequiredField("workspace.pubkey"),
    474                 )
    475             },
    476             {
    477                 let mut change = sample_change();
    478                 change.workspace.d_tag = "bad".to_string();
    479                 (change, EventEncodeError::InvalidField("workspace.d_tag"))
    480             },
    481             {
    482                 let mut change = sample_change();
    483                 change.actor_id.clear();
    484                 (change, EventEncodeError::EmptyRequiredField("actor_id"))
    485             },
    486             {
    487                 let mut change = sample_change();
    488                 change.change_hash.clear();
    489                 (change, EventEncodeError::EmptyRequiredField("change_hash"))
    490             },
    491             {
    492                 let mut change = sample_change();
    493                 change.dependencies.push(String::new());
    494                 (change, EventEncodeError::EmptyRequiredField("dependencies"))
    495             },
    496             {
    497                 let mut change = sample_change();
    498                 change.encoded_change = "abc/def".to_string();
    499                 (change, EventEncodeError::InvalidField("encoded_change"))
    500             },
    501             {
    502                 let mut change = sample_change();
    503                 change.business_time_ms = 0;
    504                 (change, EventEncodeError::InvalidField("business_time_ms"))
    505             },
    506             {
    507                 let mut change = sample_change();
    508                 change.crdt_backend_version = Some(" ".to_string());
    509                 (
    510                     change,
    511                     EventEncodeError::EmptyRequiredField("crdt_backend_version"),
    512                 )
    513             },
    514             {
    515                 let mut change = sample_change();
    516                 change.author_member_id = Some(" ".to_string());
    517                 (
    518                     change,
    519                     EventEncodeError::EmptyRequiredField("author_member_id"),
    520                 )
    521             },
    522             {
    523                 let mut change = sample_change();
    524                 change.app_version = Some(" ".to_string());
    525                 (change, EventEncodeError::EmptyRequiredField("app_version"))
    526             },
    527         ] {
    528             let err = farm_crdt_change_build_tags(&change).unwrap_err();
    529             assert_same_encode_error(err, expected);
    530         }
    531 
    532         let author_err =
    533             farm_crdt_change_build_tags_with_author(&sample_change(), Some(" ")).unwrap_err();
    534         assert_same_encode_error(
    535             author_err,
    536             EventEncodeError::EmptyRequiredField("author_pubkey"),
    537         );
    538 
    539         let wrong_kind =
    540             to_wire_parts_with_kind_and_author(&sample_change(), KIND_POST, Some(AUTHOR))
    541                 .unwrap_err();
    542         assert_same_encode_error(wrong_kind, EventEncodeError::InvalidKind(KIND_POST));
    543     }
    544 
    545     fn sample_change() -> RadrootsFarmCrdtChange {
    546         sample_change_with(
    547             DOCUMENT_ID,
    548             RadrootsFarmCrdtDocumentKind::FarmTask,
    549             RadrootsFarmSemanticKind::FarmTaskCreate,
    550         )
    551     }
    552 
    553     fn sample_change_with(
    554         document_id: &str,
    555         document_kind: RadrootsFarmCrdtDocumentKind,
    556         semantic_kind: RadrootsFarmSemanticKind,
    557     ) -> RadrootsFarmCrdtChange {
    558         RadrootsFarmCrdtChange {
    559             schema: RADROOTS_FARM_CRDT_CHANGE_SCHEMA.to_string(),
    560             workspace: RadrootsFarmWorkspaceRef {
    561                 pubkey: "workspace_pubkey".to_string(),
    562                 d_tag: WORKSPACE_D_TAG.to_string(),
    563             },
    564             farm_group_id: GROUP_ID.to_string(),
    565             document_id: document_id.to_string(),
    566             document_kind,
    567             crdt_backend: RadrootsCrdtBackend::Automerge,
    568             crdt_backend_version: Some("0.x".to_string()),
    569             actor_id: "actor_abc".to_string(),
    570             change_hash: "crdt_hash_abc".to_string(),
    571             dependencies: Vec::new(),
    572             encoded_change: "abc-DEF_012".to_string(),
    573             semantic_kind,
    574             business_time_ms: 1_780_000_000_000,
    575             author_member_id: Some("member_abc".to_string()),
    576             app_version: Some("0.1.0".to_string()),
    577         }
    578     }
    579 
    580     fn document_id(index: usize) -> String {
    581         format!("{index:02}AAAAAAAAAAAAAAAAAAAA")
    582     }
    583 
    584     fn tag(key: &str, value: &str) -> Vec<String> {
    585         vec![key.to_string(), value.to_string()]
    586     }
    587 
    588     fn remove_tags(tags: &mut Vec<Vec<String>>, name: &str) {
    589         tags.retain(|tag| tag.first().map(String::as_str) != Some(name));
    590     }
    591 
    592     fn replace_first_tag(tags: &mut [Vec<String>], name: &str, replacement: Vec<String>) {
    593         let tag = tags
    594             .iter_mut()
    595             .find(|tag| tag.first().map(String::as_str) == Some(name))
    596             .expect("tag");
    597         *tag = replacement;
    598     }
    599 
    600     fn assert_same_parse_error(actual: EventParseError, expected: EventParseError) {
    601         match (actual, expected) {
    602             (EventParseError::MissingTag(actual), EventParseError::MissingTag(expected))
    603             | (EventParseError::InvalidTag(actual), EventParseError::InvalidTag(expected))
    604             | (EventParseError::InvalidJson(actual), EventParseError::InvalidJson(expected)) => {
    605                 assert_eq!(actual, expected);
    606             }
    607             (
    608                 EventParseError::InvalidKind {
    609                     expected: actual_expected,
    610                     got: actual_got,
    611                 },
    612                 EventParseError::InvalidKind { expected, got },
    613             ) => {
    614                 assert_eq!(actual_expected, expected);
    615                 assert_eq!(actual_got, got);
    616             }
    617             (
    618                 EventParseError::InvalidNumber(actual, _),
    619                 EventParseError::InvalidNumber(expected, _),
    620             ) => {
    621                 assert_eq!(actual, expected);
    622             }
    623             (actual, expected) => {
    624                 panic!("unexpected parse error {actual:?}, expected {expected:?}")
    625             }
    626         }
    627     }
    628 
    629     fn assert_same_encode_error(actual: EventEncodeError, expected: EventEncodeError) {
    630         match (actual, expected) {
    631             (
    632                 EventEncodeError::EmptyRequiredField(actual),
    633                 EventEncodeError::EmptyRequiredField(expected),
    634             )
    635             | (EventEncodeError::InvalidField(actual), EventEncodeError::InvalidField(expected)) => {
    636                 assert_eq!(actual, expected);
    637             }
    638             (EventEncodeError::InvalidKind(actual), EventEncodeError::InvalidKind(expected)) => {
    639                 assert_eq!(actual, expected);
    640             }
    641             (EventEncodeError::Json, EventEncodeError::Json) => {}
    642             (actual, expected) => {
    643                 panic!("unexpected encode error {actual:?}, expected {expected:?}")
    644             }
    645         }
    646     }
    647 }