lib

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

follow.rs (14465B)


      1 mod common;
      2 #[path = "../src/test_fixtures.rs"]
      3 mod test_fixtures;
      4 
      5 use common::{AUTHOR, EVENT_ID, EVENT_SIG};
      6 use radroots_event::{
      7     envelope::kind::{KIND_FOLLOW, KIND_POST},
      8     social::follow::{Follow, FollowProfile},
      9 };
     10 
     11 use radroots_event_codec::decode::EventParseError;
     12 use radroots_event_codec::decode::follow::{data_from_event, follow_from_tags, parsed_from_event};
     13 use radroots_event_codec::encode::EventEncodeError;
     14 use radroots_event_codec::encode::follow::{
     15     FollowMutation, follow_apply, follow_to_wire_parts_after, to_wire_parts,
     16     to_wire_parts_with_kind,
     17 };
     18 use test_fixtures::{RELAY_PRIMARY_WSS, RELAY_SECONDARY_WSS};
     19 
     20 #[test]
     21 fn follow_to_wire_parts_builds_p_tags() {
     22     let follow = Follow {
     23         list: vec![FollowProfile {
     24             published_at: 42,
     25             public_key: "pubkey".to_string(),
     26             relay_url: Some("wss://relay".to_string()),
     27             contact_name: Some("alice".to_string()),
     28         }],
     29     };
     30 
     31     let parts = to_wire_parts(&follow).unwrap();
     32     assert_eq!(parts.kind, KIND_FOLLOW);
     33     assert_eq!(parts.content, "");
     34     assert_eq!(parts.tags.len(), 1);
     35 
     36     let tag = &parts.tags[0];
     37     assert_eq!(tag[0], "p");
     38     assert_eq!(tag[1], "pubkey");
     39     assert_eq!(tag[2], "wss://relay");
     40     assert_eq!(tag[3], "alice");
     41 }
     42 
     43 #[test]
     44 fn follow_to_wire_parts_requires_public_key() {
     45     let follow = Follow {
     46         list: vec![FollowProfile {
     47             published_at: 1,
     48             public_key: "  ".to_string(),
     49             relay_url: None,
     50             contact_name: None,
     51         }],
     52     };
     53 
     54     let err = to_wire_parts(&follow).unwrap_err();
     55     assert!(matches!(
     56         err,
     57         EventEncodeError::EmptyRequiredField("follow.public_key")
     58     ));
     59 }
     60 
     61 #[test]
     62 fn follow_from_tags_defaults_published_at() {
     63     let tags = vec![vec!["p".to_string(), "pubkey".to_string()]];
     64 
     65     let follow = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap();
     66     assert_eq!(follow.list.len(), 1);
     67     assert_eq!(follow.list[0].published_at, 123);
     68     assert_eq!(follow.list[0].public_key, "pubkey");
     69     assert!(follow.list[0].relay_url.is_none());
     70     assert!(follow.list[0].contact_name.is_none());
     71 }
     72 
     73 #[test]
     74 fn follow_from_tags_accepts_contact_without_relay() {
     75     let tags = vec![vec![
     76         "p".to_string(),
     77         "pubkey".to_string(),
     78         "alice".to_string(),
     79     ]];
     80 
     81     let follow = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap();
     82     assert_eq!(follow.list[0].published_at, 123);
     83     assert_eq!(follow.list[0].public_key, "pubkey");
     84     assert!(follow.list[0].relay_url.is_none());
     85     assert_eq!(follow.list[0].contact_name.as_deref(), Some("alice"));
     86 }
     87 
     88 #[test]
     89 fn follow_from_tags_accepts_ws_relay_and_contact_name() {
     90     let tags = vec![vec![
     91         "p".to_string(),
     92         "pubkey".to_string(),
     93         "ws://relay.example.com".to_string(),
     94         "alice".to_string(),
     95     ]];
     96 
     97     let follow = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap();
     98     assert_eq!(
     99         follow.list[0].relay_url.as_deref(),
    100         Some("ws://relay.example.com")
    101     );
    102     assert_eq!(follow.list[0].contact_name.as_deref(), Some("alice"));
    103 }
    104 
    105 #[test]
    106 fn follow_from_tags_uses_tag_published_at() {
    107     let tags = vec![vec![
    108         "p".to_string(),
    109         "pubkey".to_string(),
    110         "".to_string(),
    111         "".to_string(),
    112         "77".to_string(),
    113     ]];
    114 
    115     let follow = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap();
    116     assert_eq!(follow.list[0].published_at, 77);
    117 }
    118 
    119 #[test]
    120 fn follow_from_tags_rejects_wrong_kind() {
    121     let tags = vec![vec!["p".to_string(), "pubkey".to_string()]];
    122     let err = follow_from_tags(KIND_POST, &tags, 123).unwrap_err();
    123     assert!(matches!(
    124         err,
    125         EventParseError::InvalidKind {
    126             expected: "3",
    127             got: KIND_POST
    128         }
    129     ));
    130 }
    131 
    132 #[test]
    133 fn follow_from_tags_rejects_invalid_published_at_number() {
    134     let tags = vec![vec![
    135         "p".to_string(),
    136         "pubkey".to_string(),
    137         "".to_string(),
    138         "".to_string(),
    139         "not-a-number".to_string(),
    140     ]];
    141     let err = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap_err();
    142     assert!(matches!(err, EventParseError::InvalidNumber("p", _)));
    143 }
    144 
    145 #[test]
    146 fn follow_from_tags_rejects_missing_public_key_value() {
    147     let tags = vec![vec!["p".to_string()]];
    148     let err = follow_from_tags(KIND_FOLLOW, &tags, 123).unwrap_err();
    149     assert!(matches!(err, EventParseError::InvalidTag("p")));
    150 }
    151 
    152 #[test]
    153 fn follow_metadata_and_index_from_event_roundtrip() {
    154     let tags = vec![vec![
    155         "p".to_string(),
    156         "pubkey".to_string(),
    157         RELAY_PRIMARY_WSS.to_string(),
    158         "alice".to_string(),
    159         "88".to_string(),
    160     ]];
    161     let metadata = data_from_event(
    162         "id".to_string(),
    163         "author".to_string(),
    164         50,
    165         KIND_FOLLOW,
    166         "".to_string(),
    167         tags.clone(),
    168     )
    169     .unwrap();
    170     assert_eq!(metadata.id, "id");
    171     assert_eq!(metadata.author, "author");
    172     assert_eq!(metadata.published_at, 50);
    173     assert_eq!(metadata.kind, KIND_FOLLOW);
    174     assert_eq!(metadata.data.list.len(), 1);
    175     assert_eq!(metadata.data.list[0].published_at, 88);
    176     assert_eq!(metadata.data.list[0].public_key, "pubkey");
    177     assert_eq!(
    178         metadata.data.list[0].relay_url.as_deref(),
    179         Some(RELAY_PRIMARY_WSS)
    180     );
    181     assert_eq!(metadata.data.list[0].contact_name.as_deref(), Some("alice"));
    182 
    183     let index = parsed_from_event(
    184         EVENT_ID.to_string(),
    185         AUTHOR.to_string(),
    186         50,
    187         KIND_FOLLOW,
    188         "".to_string(),
    189         tags,
    190         EVENT_SIG.to_string(),
    191     )
    192     .unwrap();
    193     assert_eq!(index.event.kind_u32(), KIND_FOLLOW);
    194     assert_eq!(index.event.signature_hex(), EVENT_SIG);
    195     assert_eq!(index.data.data.list.len(), 1);
    196 }
    197 
    198 #[test]
    199 fn follow_index_from_event_propagates_parse_errors() {
    200     let err = parsed_from_event(
    201         "id".to_string(),
    202         "author".to_string(),
    203         50,
    204         KIND_POST,
    205         "".to_string(),
    206         Vec::new(),
    207         "sig".to_string(),
    208     )
    209     .unwrap_err();
    210     assert!(matches!(
    211         err,
    212         EventParseError::InvalidKind {
    213             expected: "3",
    214             got: KIND_POST
    215         }
    216     ));
    217 }
    218 
    219 #[test]
    220 fn follow_apply_adds_and_updates_entries() {
    221     let follow = Follow {
    222         list: vec![
    223             FollowProfile {
    224                 published_at: 1,
    225                 public_key: "pubkey-a".to_string(),
    226                 relay_url: None,
    227                 contact_name: Some("alice".to_string()),
    228             },
    229             FollowProfile {
    230                 published_at: 1,
    231                 public_key: "pubkey-b".to_string(),
    232                 relay_url: None,
    233                 contact_name: Some("bob".to_string()),
    234             },
    235         ],
    236     };
    237 
    238     let updated = follow_apply(
    239         &follow,
    240         FollowMutation::Follow {
    241             public_key: "pubkey-a".to_string(),
    242             relay_url: Some("wss://relay".to_string()),
    243             contact_name: Some("alice-updated".to_string()),
    244         },
    245     )
    246     .unwrap();
    247     assert_eq!(updated.list.len(), 2);
    248     assert_eq!(updated.list[0].public_key, "pubkey-a");
    249     assert_eq!(updated.list[0].relay_url.as_deref(), Some("wss://relay"));
    250     assert_eq!(
    251         updated.list[0].contact_name.as_deref(),
    252         Some("alice-updated")
    253     );
    254 
    255     let added = follow_apply(
    256         &follow,
    257         FollowMutation::Follow {
    258             public_key: "pubkey-c".to_string(),
    259             relay_url: None,
    260             contact_name: Some("cara".to_string()),
    261         },
    262     )
    263     .unwrap();
    264     assert_eq!(added.list.len(), 3);
    265     assert_eq!(added.list[2].public_key, "pubkey-c");
    266 }
    267 
    268 #[test]
    269 fn follow_apply_unfollow_removes_entries() {
    270     let follow = Follow {
    271         list: vec![
    272             FollowProfile {
    273                 published_at: 1,
    274                 public_key: "pubkey-a".to_string(),
    275                 relay_url: None,
    276                 contact_name: None,
    277             },
    278             FollowProfile {
    279                 published_at: 1,
    280                 public_key: "pubkey-b".to_string(),
    281                 relay_url: None,
    282                 contact_name: None,
    283             },
    284         ],
    285     };
    286 
    287     let removed = follow_apply(
    288         &follow,
    289         FollowMutation::Unfollow {
    290             public_key: "pubkey-b".to_string(),
    291         },
    292     )
    293     .unwrap();
    294     assert_eq!(removed.list.len(), 1);
    295     assert_eq!(removed.list[0].public_key, "pubkey-a");
    296 }
    297 
    298 #[test]
    299 fn follow_apply_toggle_adds_or_removes() {
    300     let follow = Follow {
    301         list: vec![FollowProfile {
    302             published_at: 1,
    303             public_key: "pubkey-a".to_string(),
    304             relay_url: None,
    305             contact_name: None,
    306         }],
    307     };
    308 
    309     let removed = follow_apply(
    310         &follow,
    311         FollowMutation::Toggle {
    312             public_key: "pubkey-a".to_string(),
    313             relay_url: None,
    314             contact_name: None,
    315         },
    316     )
    317     .unwrap();
    318     assert!(removed.list.is_empty());
    319 
    320     let added = follow_apply(
    321         &follow,
    322         FollowMutation::Toggle {
    323             public_key: "pubkey-b".to_string(),
    324             relay_url: None,
    325             contact_name: Some("bob".to_string()),
    326         },
    327     )
    328     .unwrap();
    329     assert_eq!(added.list.len(), 2);
    330     assert_eq!(added.list[1].public_key, "pubkey-b");
    331 }
    332 
    333 #[test]
    334 fn follow_apply_rejects_empty_pubkey() {
    335     let follow = Follow { list: Vec::new() };
    336     let err = follow_apply(
    337         &follow,
    338         FollowMutation::Follow {
    339             public_key: "  ".to_string(),
    340             relay_url: None,
    341             contact_name: None,
    342         },
    343     )
    344     .unwrap_err();
    345     assert!(matches!(
    346         err,
    347         EventEncodeError::EmptyRequiredField("follow.public_key")
    348     ));
    349 }
    350 
    351 #[test]
    352 fn follow_apply_rejects_empty_pubkey_for_unfollow_and_toggle() {
    353     let follow = Follow { list: Vec::new() };
    354     let err = follow_apply(
    355         &follow,
    356         FollowMutation::Unfollow {
    357             public_key: "  ".to_string(),
    358         },
    359     )
    360     .unwrap_err();
    361     assert!(matches!(
    362         err,
    363         EventEncodeError::EmptyRequiredField("follow.public_key")
    364     ));
    365 
    366     let err = follow_apply(
    367         &follow,
    368         FollowMutation::Toggle {
    369             public_key: "  ".to_string(),
    370             relay_url: None,
    371             contact_name: None,
    372         },
    373     )
    374     .unwrap_err();
    375     assert!(matches!(
    376         err,
    377         EventEncodeError::EmptyRequiredField("follow.public_key")
    378     ));
    379 }
    380 
    381 #[test]
    382 fn follow_apply_rejects_invalid_existing_entries_and_after_mutation_propagates_error() {
    383     let follow = Follow {
    384         list: vec![FollowProfile {
    385             published_at: 1,
    386             public_key: " ".to_string(),
    387             relay_url: None,
    388             contact_name: None,
    389         }],
    390     };
    391 
    392     let err = follow_apply(
    393         &follow,
    394         FollowMutation::Unfollow {
    395             public_key: "pubkey-a".to_string(),
    396         },
    397     )
    398     .unwrap_err();
    399     assert!(matches!(
    400         err,
    401         EventEncodeError::EmptyRequiredField("follow.public_key")
    402     ));
    403 
    404     let err = follow_to_wire_parts_after(
    405         &Follow { list: Vec::new() },
    406         FollowMutation::Follow {
    407             public_key: " ".to_string(),
    408             relay_url: None,
    409             contact_name: None,
    410         },
    411     )
    412     .unwrap_err();
    413     assert!(matches!(
    414         err,
    415         EventEncodeError::EmptyRequiredField("follow.public_key")
    416     ));
    417 }
    418 
    419 #[test]
    420 fn follow_build_tags_normalizes_empty_optional_values() {
    421     let follow = Follow {
    422         list: vec![FollowProfile {
    423             published_at: 1,
    424             public_key: "pubkey".to_string(),
    425             relay_url: Some("".to_string()),
    426             contact_name: Some(" ".to_string()),
    427         }],
    428     };
    429     let parts = to_wire_parts(&follow).unwrap();
    430     assert_eq!(
    431         parts.tags,
    432         vec![vec!["p".to_string(), "pubkey".to_string(), " ".to_string()]]
    433     );
    434 }
    435 
    436 #[test]
    437 fn follow_to_wire_parts_with_kind_and_after_mutation_work() {
    438     let follow = Follow {
    439         list: vec![FollowProfile {
    440             published_at: 1,
    441             public_key: "pubkey-a".to_string(),
    442             relay_url: None,
    443             contact_name: None,
    444         }],
    445     };
    446     let parts = to_wire_parts_with_kind(&follow, KIND_POST).unwrap();
    447     assert_eq!(parts.kind, KIND_POST);
    448 
    449     let toggled = follow_to_wire_parts_after(
    450         &follow,
    451         FollowMutation::Toggle {
    452             public_key: "pubkey-b".to_string(),
    453             relay_url: Some(RELAY_PRIMARY_WSS.to_string()),
    454             contact_name: Some("alice".to_string()),
    455         },
    456     )
    457     .unwrap();
    458     assert_eq!(toggled.kind, KIND_FOLLOW);
    459     assert_eq!(toggled.tags.len(), 2);
    460 }
    461 
    462 #[test]
    463 fn follow_apply_normalizes_optional_fields_and_deduplicates_existing_list() {
    464     let follow = Follow {
    465         list: vec![
    466             FollowProfile {
    467                 published_at: 1,
    468                 public_key: " pubkey-a ".to_string(),
    469                 relay_url: Some(" ".to_string()),
    470                 contact_name: Some(" ".to_string()),
    471             },
    472             FollowProfile {
    473                 published_at: 2,
    474                 public_key: "pubkey-a".to_string(),
    475                 relay_url: Some(RELAY_SECONDARY_WSS.to_string()),
    476                 contact_name: Some("duplicate".to_string()),
    477             },
    478         ],
    479     };
    480 
    481     let updated = follow_apply(
    482         &follow,
    483         FollowMutation::Follow {
    484             public_key: "pubkey-a".to_string(),
    485             relay_url: Some(" ".to_string()),
    486             contact_name: Some(" ".to_string()),
    487         },
    488     )
    489     .unwrap();
    490 
    491     assert_eq!(updated.list.len(), 1);
    492     assert_eq!(updated.list[0].public_key, "pubkey-a");
    493     assert!(updated.list[0].relay_url.is_none());
    494     assert!(updated.list[0].contact_name.is_none());
    495 }
    496 
    497 #[test]
    498 fn follow_apply_follow_with_none_preserves_existing_values() {
    499     let follow = Follow {
    500         list: vec![FollowProfile {
    501             published_at: 1,
    502             public_key: "pubkey-a".to_string(),
    503             relay_url: Some(RELAY_PRIMARY_WSS.to_string()),
    504             contact_name: Some("alice".to_string()),
    505         }],
    506     };
    507 
    508     let updated = follow_apply(
    509         &follow,
    510         FollowMutation::Follow {
    511             public_key: "pubkey-a".to_string(),
    512             relay_url: None,
    513             contact_name: None,
    514         },
    515     )
    516     .unwrap();
    517     assert_eq!(updated.list.len(), 1);
    518     assert_eq!(
    519         updated.list[0].relay_url.as_deref(),
    520         Some(RELAY_PRIMARY_WSS)
    521     );
    522     assert_eq!(updated.list[0].contact_name.as_deref(), Some("alice"));
    523 }