lib

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

article.rs (7228B)


      1 #![cfg(feature = "json")]
      2 
      3 mod common;
      4 
      5 use common::{AUTHOR, EVENT_ID, EVENT_SIG};
      6 use radroots_event::{
      7     envelope::kind::{KIND_ARTICLE, KIND_POST},
      8     farm::FarmRef,
      9     post::article::Article,
     10     social::{SocialFarmAnchor, SocialLocation},
     11     tag::name::{TAG_A, TAG_D, TAG_G, TAG_IMAGE, TAG_LOCATION, TAG_PUBLISHED_AT, TAG_T, TAG_TITLE},
     12 };
     13 use radroots_event_codec::decode::{
     14     EventParseError,
     15     article::{article_from_event, data_from_event, parsed_from_event},
     16 };
     17 use radroots_event_codec::encode::{
     18     EventEncodeError,
     19     article::{article_build_tags, to_wire_parts, to_wire_parts_with_kind},
     20 };
     21 
     22 const VALID_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA";
     23 const FARM_D_TAG: &str = "BBBBBBBBBBBBBBBBBBBBBA";
     24 const FARM_PUBKEY: &str = "farm_pubkey";
     25 
     26 fn sample_article() -> Article {
     27     Article {
     28         d_tag: VALID_D_TAG.to_string(),
     29         title: "Spring soil notes".to_string(),
     30         content: "# Spring soil notes".to_string(),
     31         summary: Some("Field update".to_string()),
     32         image: Some("https://media.example.test/soil.jpg".to_string()),
     33         published_at: Some(1_781_895_600),
     34         farm: Some(SocialFarmAnchor {
     35             farm: FarmRef {
     36                 pubkey: FARM_PUBKEY.to_string(),
     37                 d_tag: FARM_D_TAG.to_string(),
     38             },
     39             relays: None,
     40         }),
     41         location: Some(SocialLocation {
     42             name: Some("North field".to_string()),
     43             geohash: Some("c23nb62w20st".to_string()),
     44         }),
     45         topics: Some(vec!["soil".to_string(), "cover-crops".to_string()]),
     46     }
     47 }
     48 
     49 fn has_tag(tags: &[Vec<String>], key: &str, value: &str) -> bool {
     50     tags.iter().any(|tag| {
     51         tag.first().map(|entry| entry.as_str()) == Some(key)
     52             && tag.get(1).map(|entry| entry.as_str()) == Some(value)
     53     })
     54 }
     55 
     56 #[test]
     57 fn article_to_wire_parts_roundtrips_social_metadata() {
     58     let article = sample_article();
     59     let parts = to_wire_parts(&article).unwrap();
     60 
     61     assert_eq!(parts.kind, KIND_ARTICLE);
     62     assert_eq!(parts.content, article.content);
     63     assert!(has_tag(&parts.tags, TAG_D, VALID_D_TAG));
     64     assert!(has_tag(&parts.tags, TAG_TITLE, "Spring soil notes"));
     65     assert!(has_tag(
     66         &parts.tags,
     67         TAG_IMAGE,
     68         "https://media.example.test/soil.jpg"
     69     ));
     70     assert!(has_tag(&parts.tags, TAG_PUBLISHED_AT, "1781895600"));
     71     assert!(has_tag(&parts.tags, TAG_LOCATION, "North field"));
     72     assert!(has_tag(&parts.tags, TAG_G, "c23nb62w20st"));
     73     assert!(has_tag(
     74         &parts.tags,
     75         TAG_A,
     76         "30340:farm_pubkey:BBBBBBBBBBBBBBBBBBBBBA"
     77     ));
     78     assert!(has_tag(&parts.tags, TAG_T, "soil"));
     79     assert!(has_tag(&parts.tags, TAG_T, "cover-crops"));
     80 
     81     let decoded = article_from_event(parts.kind, &parts.tags, &parts.content).unwrap();
     82     assert_eq!(decoded.d_tag, VALID_D_TAG);
     83     assert_eq!(decoded.title, "Spring soil notes");
     84     assert_eq!(decoded.content, "# Spring soil notes");
     85     assert_eq!(decoded.summary.as_deref(), Some("Field update"));
     86     assert_eq!(decoded.published_at, Some(1_781_895_600));
     87     assert_eq!(
     88         decoded.farm.as_ref().map(|farm| farm.farm.pubkey.as_str()),
     89         Some(FARM_PUBKEY)
     90     );
     91     assert_eq!(
     92         decoded
     93             .location
     94             .as_ref()
     95             .and_then(|location| location.name.as_deref()),
     96         Some("North field")
     97     );
     98     assert_eq!(decoded.topics.as_ref().map(Vec::len), Some(2));
     99 }
    100 
    101 #[test]
    102 fn article_codec_requires_kind_required_fields_and_valid_d_tag() {
    103     let mut article = sample_article();
    104     article.title = " ".to_string();
    105     assert!(matches!(
    106         article_build_tags(&article),
    107         Err(EventEncodeError::EmptyRequiredField("title"))
    108     ));
    109 
    110     let mut article = sample_article();
    111     article.d_tag = "bad".to_string();
    112     assert!(matches!(
    113         to_wire_parts(&article),
    114         Err(EventEncodeError::InvalidField("d_tag"))
    115     ));
    116 
    117     assert!(matches!(
    118         to_wire_parts_with_kind(&sample_article(), KIND_POST),
    119         Err(EventEncodeError::InvalidKind(KIND_POST))
    120     ));
    121 
    122     let mut tags = article_build_tags(&sample_article()).unwrap();
    123     tags.retain(|tag| tag.first().map(|value| value.as_str()) != Some(TAG_TITLE));
    124     assert!(matches!(
    125         article_from_event(KIND_ARTICLE, &tags, "# Spring soil notes"),
    126         Err(EventParseError::MissingTag(TAG_TITLE))
    127     ));
    128 
    129     let err = article_from_event(KIND_POST, &tags, "# Spring soil notes").unwrap_err();
    130     assert!(matches!(
    131         err,
    132         EventParseError::InvalidKind {
    133             expected: "30023",
    134             got: KIND_POST
    135         }
    136     ));
    137 }
    138 
    139 #[test]
    140 fn article_decode_handles_minimal_and_invalid_optional_tags() {
    141     let tags = vec![
    142         vec![TAG_D.to_string(), VALID_D_TAG.to_string()],
    143         vec![TAG_TITLE.to_string(), "Minimal article".to_string()],
    144     ];
    145     let decoded = article_from_event(KIND_ARTICLE, &tags, "Body").unwrap();
    146     assert_eq!(decoded.d_tag, VALID_D_TAG);
    147     assert_eq!(decoded.title, "Minimal article");
    148     assert!(decoded.farm.is_none());
    149     assert_eq!(decoded.topics, None);
    150     assert_eq!(decoded.published_at, None);
    151 
    152     let mut tags = tags.clone();
    153     tags.push(vec![TAG_PUBLISHED_AT.to_string(), "not-a-time".to_string()]);
    154     assert!(matches!(
    155         article_from_event(KIND_ARTICLE, &tags, "Body"),
    156         Err(EventParseError::InvalidNumber(TAG_PUBLISHED_AT, _))
    157     ));
    158 
    159     assert!(matches!(
    160         article_from_event(KIND_ARTICLE, &tags, " "),
    161         Err(EventParseError::InvalidTag("content"))
    162     ));
    163 }
    164 
    165 #[test]
    166 fn article_build_tags_handles_absent_optional_metadata() {
    167     let article = Article {
    168         d_tag: VALID_D_TAG.to_string(),
    169         title: "Minimal article".to_string(),
    170         content: "Body".to_string(),
    171         summary: None,
    172         image: None,
    173         published_at: None,
    174         farm: None,
    175         location: None,
    176         topics: None,
    177     };
    178 
    179     let tags = article_build_tags(&article).unwrap();
    180     assert!(has_tag(&tags, TAG_D, VALID_D_TAG));
    181     assert!(has_tag(&tags, TAG_TITLE, "Minimal article"));
    182     assert!(!tags.iter().any(|tag| {
    183         matches!(
    184             tag.first().map(String::as_str),
    185             Some(TAG_PUBLISHED_AT | TAG_A | TAG_LOCATION | TAG_G | TAG_T)
    186         )
    187     }));
    188 }
    189 
    190 #[test]
    191 fn article_wrappers_preserve_event_metadata() {
    192     let article = sample_article();
    193     let parts = to_wire_parts(&article).unwrap();
    194     let data = data_from_event(
    195         "event_id".to_string(),
    196         "author".to_string(),
    197         42,
    198         parts.kind,
    199         parts.content.clone(),
    200         parts.tags.clone(),
    201     )
    202     .unwrap();
    203 
    204     assert_eq!(data.id, "event_id");
    205     assert_eq!(data.author, "author");
    206     assert_eq!(data.published_at, 42);
    207     assert_eq!(data.kind, KIND_ARTICLE);
    208     assert_eq!(data.data.title, "Spring soil notes");
    209 
    210     let parsed = parsed_from_event(
    211         EVENT_ID.to_string(),
    212         AUTHOR.to_string(),
    213         42,
    214         parts.kind,
    215         parts.content,
    216         parts.tags,
    217         EVENT_SIG.to_string(),
    218     )
    219     .unwrap();
    220 
    221     assert_eq!(parsed.event.id_hex(), EVENT_ID);
    222     assert_eq!(parsed.event.author().to_hex(), AUTHOR);
    223     assert_eq!(parsed.event.signature_hex(), EVENT_SIG);
    224     assert_eq!(parsed.data.data.d_tag, VALID_D_TAG);
    225 }