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