lib

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

follow.rs (14560B)


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