lib

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

tests.rs (11766B)


      1 use super::*;
      2 use crate::contract::ContractMatchError;
      3 use crate::envelope::EventEnvelopeParts;
      4 use crate::envelope::kind::{
      5     KIND_FOLLOW, KIND_LIST_SET_GENERIC, KIND_POST, KIND_PROFILE, KIND_TRADE_PROPOSAL,
      6 };
      7 use crate::id::parse_public_key;
      8 
      9 fn hex_64(character: char) -> String {
     10     crate::test_valid_hex_64(character)
     11 }
     12 
     13 fn hex_128(character: char) -> String {
     14     core::iter::repeat_n(character, 128).collect()
     15 }
     16 
     17 fn event(
     18     kind: u32,
     19     id: &str,
     20     author: &str,
     21     created_at: u64,
     22     tags: Vec<Vec<String>>,
     23 ) -> EventEnvelope {
     24     EventEnvelope::new(EventEnvelopeParts {
     25         id: id.to_string(),
     26         author: author.to_string(),
     27         created_at,
     28         kind,
     29         tags,
     30         content: String::new(),
     31         sig: hex_128('f'),
     32     })
     33     .expect("event envelope")
     34 }
     35 
     36 fn event_with_content(
     37     kind: u32,
     38     id: &str,
     39     author: &str,
     40     created_at: u64,
     41     tags: Vec<Vec<String>>,
     42     content: &str,
     43 ) -> EventEnvelope {
     44     EventEnvelope::new(EventEnvelopeParts {
     45         id: id.to_string(),
     46         author: author.to_string(),
     47         created_at,
     48         kind,
     49         tags,
     50         content: content.to_string(),
     51         sig: hex_128('f'),
     52     })
     53     .expect("event envelope")
     54 }
     55 
     56 fn candidate(id: char, created_at: u64) -> EventHeadCandidate {
     57     expect_candidate(event_head_candidate_for_class(
     58         &event(10002, &hex_64(id), &hex_64('a'), created_at, Vec::new()),
     59         EventClass::Replaceable,
     60     ))
     61 }
     62 
     63 fn expect_candidate(result: EventHeadCandidateResult) -> EventHeadCandidate {
     64     match result {
     65         EventHeadCandidateResult::Candidate(candidate) => candidate,
     66         other => panic!("expected candidate: {other:?}"),
     67     }
     68 }
     69 
     70 #[test]
     71 fn regular_and_ephemeral_events_do_not_create_heads() {
     72     let event = event(1, &hex_64('1'), &hex_64('a'), 1, Vec::new());
     73     assert_eq!(
     74         event_head_candidate_for_class(&event, EventClass::Regular),
     75         EventHeadCandidateResult::NotHeadSelected
     76     );
     77     assert_eq!(
     78         event_head_candidate_for_class(&event, EventClass::Ephemeral),
     79         EventHeadCandidateResult::NotPersisted
     80     );
     81 }
     82 
     83 #[test]
     84 fn replaceable_events_use_kind_and_pubkey_coordinates() {
     85     let event = event(10002, &hex_64('1'), &hex_64('a'), 5, Vec::new());
     86     let candidate = expect_candidate(event_head_candidate_for_class(
     87         &event,
     88         EventClass::Replaceable,
     89     ));
     90     assert_eq!(
     91         candidate.coordinate,
     92         EventHeadCoordinate::Replaceable {
     93             kind: 10002,
     94             pubkey: parse_public_key(hex_64('a')).unwrap()
     95         }
     96     );
     97     assert_eq!(candidate.created_at, 5);
     98 }
     99 
    100 #[test]
    101 fn addressable_events_use_kind_pubkey_and_d_tag_coordinates() {
    102     let event = event(
    103         30023,
    104         &hex_64('2'),
    105         &hex_64('b'),
    106         7,
    107         vec![vec![TAG_D.to_string(), "article-1".to_string()]],
    108     );
    109     let candidate = expect_candidate(event_head_candidate_for_class(
    110         &event,
    111         EventClass::Addressable,
    112     ));
    113     assert_eq!(
    114         candidate.coordinate,
    115         EventHeadCoordinate::Addressable {
    116             kind: 30023,
    117             pubkey: parse_public_key(hex_64('b')).unwrap(),
    118             d_tag: "article-1".to_owned()
    119         }
    120     );
    121 }
    122 
    123 #[test]
    124 fn addressable_events_require_valid_d_tags() {
    125     let missing = event(30023, &hex_64('2'), &hex_64('b'), 7, Vec::new());
    126     assert_eq!(
    127         event_head_candidate_for_class(&missing, EventClass::Addressable),
    128         EventHeadCandidateResult::Malformed(EventHeadMalformed::MissingDTag)
    129     );
    130 
    131     let invalid = event(
    132         30023,
    133         &hex_64('2'),
    134         &hex_64('b'),
    135         7,
    136         vec![vec![TAG_D.to_string(), "bad d".to_string()]],
    137     );
    138     assert!(matches!(
    139         event_head_candidate_for_class(&invalid, EventClass::Addressable),
    140         EventHeadCandidateResult::Malformed(EventHeadMalformed::InvalidDTag(_))
    141     ));
    142 }
    143 
    144 #[test]
    145 fn event_head_selection_uses_nip01_time_and_lowest_id_rules() {
    146     let current: CurrentEventHead = candidate('3', 10).into();
    147 
    148     assert!(matches!(
    149         select_event_head(candidate('1', 1), None),
    150         EventHeadDecision::Applied(_)
    151     ));
    152     assert!(matches!(
    153         select_event_head(candidate('4', 11), Some(&current)),
    154         EventHeadDecision::Applied(_)
    155     ));
    156     assert_eq!(
    157         select_event_head(candidate('2', 9), Some(&current)),
    158         EventHeadDecision::SkippedOlder
    159     );
    160     assert_eq!(
    161         select_event_head(candidate('3', 10), Some(&current)),
    162         EventHeadDecision::SkippedDuplicate
    163     );
    164     assert!(matches!(
    165         select_event_head(candidate('2', 10), Some(&current)),
    166         EventHeadDecision::Applied(_)
    167     ));
    168     assert_eq!(
    169         select_event_head(candidate('4', 10), Some(&current)),
    170         EventHeadDecision::SkippedSameTimestampHigherEventId
    171     );
    172 }
    173 
    174 #[test]
    175 fn event_head_selection_rejects_coordinate_mismatch() {
    176     let current: CurrentEventHead = candidate('3', 10).into();
    177     let other = event_head_candidate_for_class(
    178         &event(
    179             30023,
    180             &hex_64('2'),
    181             &hex_64('a'),
    182             11,
    183             vec![vec![TAG_D.to_string(), "article".to_string()]],
    184         ),
    185         EventClass::Addressable,
    186     );
    187     let other = expect_candidate(other);
    188     assert_eq!(
    189         select_event_head(other, Some(&current)),
    190         EventHeadDecision::CoordinateMismatch
    191     );
    192 }
    193 
    194 #[test]
    195 fn contract_bridge_uses_replaceable_event_classes() {
    196     let event = event(KIND_FOLLOW, &hex_64('1'), &hex_64('a'), 1, Vec::new());
    197     let candidate = expect_candidate(event_head_candidate_for_event(&event).expect("contract"));
    198     assert_eq!(
    199         candidate.coordinate,
    200         EventHeadCoordinate::Replaceable {
    201             kind: KIND_FOLLOW,
    202             pubkey: parse_public_key(hex_64('a')).unwrap()
    203         }
    204     );
    205 }
    206 
    207 #[test]
    208 fn raw_nip01_bridge_uses_numeric_kind_classes_without_contract_identification() {
    209     let replaceable = event(19_999, &hex_64('1'), &hex_64('a'), 1, Vec::new());
    210     let replaceable = expect_candidate(event_head_candidate_for_nip01_event(&replaceable));
    211     assert_eq!(
    212         replaceable.coordinate,
    213         EventHeadCoordinate::Replaceable {
    214             kind: 19_999,
    215             pubkey: parse_public_key(hex_64('a')).unwrap(),
    216         }
    217     );
    218 
    219     let addressable = event(
    220         39_999,
    221         &hex_64('2'),
    222         &hex_64('b'),
    223         2,
    224         vec![vec![TAG_D.to_string(), "unsupported".to_string()]],
    225     );
    226     let addressable = expect_candidate(event_head_candidate_for_nip01_event(&addressable));
    227     assert_eq!(
    228         addressable.coordinate,
    229         EventHeadCoordinate::Addressable {
    230             kind: 39_999,
    231             pubkey: parse_public_key(hex_64('b')).unwrap(),
    232             d_tag: "unsupported".to_owned(),
    233         }
    234     );
    235 
    236     let regular = event(40_000, &hex_64('3'), &hex_64('c'), 3, Vec::new());
    237     assert_eq!(
    238         event_head_candidate_for_nip01_event(&regular),
    239         EventHeadCandidateResult::NotHeadSelected
    240     );
    241 }
    242 
    243 #[test]
    244 fn raw_nip01_addressable_coordinates_treat_d_as_opaque_protocol_data() {
    245     for (tags, expected) in [
    246         (Vec::new(), ""),
    247         (vec![vec![TAG_D.to_owned(), String::new()]], ""),
    248         (
    249             vec![
    250                 vec![TAG_D.to_owned()],
    251                 vec![TAG_D.to_owned(), "ignored".to_owned()],
    252             ],
    253             "",
    254         ),
    255         (
    256             vec![vec![TAG_D.to_owned(), "not a product d".to_owned()]],
    257             "not a product d",
    258         ),
    259         (
    260             vec![vec![TAG_D.to_owned(), "line\nbreak".to_owned()]],
    261             "line\nbreak",
    262         ),
    263         (
    264             vec![
    265                 vec![TAG_D.to_owned(), "first value".to_owned()],
    266                 vec![TAG_D.to_owned(), "second-value".to_owned()],
    267             ],
    268             "first value",
    269         ),
    270     ] {
    271         let event = event(39_999, &hex_64('2'), &hex_64('b'), 2, tags);
    272         let candidate = expect_candidate(event_head_candidate_for_nip01_event(&event));
    273         assert_eq!(
    274             candidate.coordinate,
    275             EventHeadCoordinate::Addressable {
    276                 kind: 39_999,
    277                 pubkey: parse_public_key(hex_64('b')).unwrap(),
    278                 d_tag: expected.to_owned(),
    279             }
    280         );
    281     }
    282 }
    283 
    284 #[test]
    285 fn product_addressable_coordinates_retain_strict_d_validation() {
    286     for tags in [
    287         Vec::new(),
    288         vec![vec![TAG_D.to_owned(), String::new()]],
    289         vec![vec![TAG_D.to_owned(), "not a product d".to_owned()]],
    290     ] {
    291         let event = event(30_023, &hex_64('2'), &hex_64('b'), 2, tags);
    292         assert!(matches!(
    293             event_head_candidate_for_class(&event, EventClass::Addressable),
    294             EventHeadCandidateResult::Malformed(_)
    295         ));
    296     }
    297 }
    298 
    299 #[test]
    300 fn contract_bridge_uses_addressable_event_classes() {
    301     let event = event(
    302         KIND_LIST_SET_GENERIC,
    303         &hex_64('2'),
    304         &hex_64('b'),
    305         1,
    306         vec![vec![TAG_D.to_string(), "member_of.farms".to_string()]],
    307     );
    308     let candidate = expect_candidate(event_head_candidate_for_event(&event).expect("contract"));
    309     assert_eq!(
    310         candidate.coordinate,
    311         EventHeadCoordinate::Addressable {
    312             kind: KIND_LIST_SET_GENERIC,
    313             pubkey: parse_public_key(hex_64('b')).unwrap(),
    314             d_tag: "member_of.farms".to_owned()
    315         }
    316     );
    317 }
    318 
    319 #[test]
    320 fn contract_bridge_uses_profile_replaceable_heads() {
    321     let profile = event_with_content(
    322         KIND_PROFILE,
    323         &hex_64('3'),
    324         &hex_64('c'),
    325         1,
    326         Vec::new(),
    327         r#"{"name":"Alice"}"#,
    328     );
    329     let candidate =
    330         expect_candidate(event_head_candidate_for_event(&profile).expect("profile contract"));
    331     assert_eq!(
    332         candidate.coordinate,
    333         EventHeadCoordinate::Replaceable {
    334             kind: KIND_PROFILE,
    335             pubkey: parse_public_key(hex_64('c')).unwrap()
    336         }
    337     );
    338 }
    339 
    340 #[test]
    341 fn contract_bridge_keeps_trade_mutations_out_of_head_selection() {
    342     let trade = event_with_content(
    343         KIND_TRADE_PROPOSAL,
    344         &hex_64('4'),
    345         &hex_64('d'),
    346         1,
    347         vec![
    348             vec![
    349                 "contract".to_string(),
    350                 "radroots.trade.proposal.v1".to_string(),
    351             ],
    352             vec!["p".to_string(), hex_64('e')],
    353             vec![
    354                 TAG_D.to_string(),
    355                 "11111111111111111111111111111111".to_string(),
    356             ],
    357         ],
    358         r#"{"contract_id":"radroots.trade.proposal.v1"}"#,
    359     );
    360     assert_eq!(
    361         event_head_candidate_for_event(&trade).expect("trade contract"),
    362         EventHeadCandidateResult::NotHeadSelected
    363     );
    364 }
    365 
    366 #[test]
    367 fn contract_bridge_reports_unsupported_and_malformed_shapes() {
    368     let unsupported = event(999_999, &hex_64('5'), &hex_64('a'), 1, Vec::new());
    369     assert_eq!(
    370         event_head_candidate_for_event(&unsupported),
    371         Err(ContractMatchError::UnsupportedKind(999_999))
    372     );
    373 
    374     let malformed_addressable = event(
    375         KIND_LIST_SET_GENERIC,
    376         &hex_64('6'),
    377         &hex_64('a'),
    378         1,
    379         Vec::new(),
    380     );
    381     assert_eq!(
    382         event_head_candidate_for_event(&malformed_addressable),
    383         Err(ContractMatchError::UnsupportedShape(KIND_LIST_SET_GENERIC))
    384     );
    385 
    386     let regular_with_d_tag = event(
    387         KIND_POST,
    388         &hex_64('7'),
    389         &hex_64('a'),
    390         1,
    391         vec![vec![TAG_D.to_string(), "not-a-head".to_string()]],
    392     );
    393     assert_eq!(
    394         event_head_candidate_for_event(&regular_with_d_tag).expect("post contract"),
    395         EventHeadCandidateResult::NotHeadSelected
    396     );
    397 }
    398 
    399 #[test]
    400 fn expect_candidate_reports_non_candidate_inputs() {
    401     let result = std::panic::catch_unwind(|| {
    402         expect_candidate(EventHeadCandidateResult::NotHeadSelected);
    403     });
    404 
    405     assert!(result.is_err());
    406 }