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 }