mod.rs (18475B)
1 pub mod decode; 2 pub mod encode; 3 4 #[cfg(test)] 5 mod tests { 6 use radroots_events::{ 7 farm_crdt::RadrootsFarmCrdtDocumentKind, 8 farm_file::{ 9 KIND_FARM_FILE_METADATA, RadrootsFarmFileDimensions, RadrootsFarmFileMetadata, 10 RadrootsFarmFileSource, 11 }, 12 farm_workspace::RadrootsFarmWorkspaceRef, 13 kinds::KIND_POST, 14 }; 15 16 use crate::error::{EventEncodeError, EventParseError}; 17 use crate::farm_file::decode::{ 18 data_from_event, farm_file_metadata_from_event, parsed_from_event, 19 }; 20 use crate::farm_file::encode::{ 21 farm_file_metadata_build_tags, to_wire_parts, to_wire_parts_with_kind, 22 }; 23 24 const FILE_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ"; 25 const WORKSPACE_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA"; 26 const OWNER_DOCUMENT_ID: &str = "AAAAAAAAAAAAAAAAAAAAAg"; 27 const GROUP_ID: &str = "field-group"; 28 const SHA256: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; 29 30 #[test] 31 fn farm_file_metadata_encodes_tags_and_caption_content() { 32 let metadata = sample_metadata(); 33 let parts = to_wire_parts(&metadata).expect("file metadata wire parts"); 34 35 assert_eq!(parts.kind, KIND_FARM_FILE_METADATA); 36 assert_eq!(parts.content, "Tomatoes harvested from Patch Y."); 37 assert!(parts.tags.contains(&tag("d", FILE_D_TAG))); 38 assert!(parts.tags.contains(&tag("h", GROUP_ID))); 39 assert!( 40 parts 41 .tags 42 .contains(&tag("a", "30078:workspace_pubkey:AAAAAAAAAAAAAAAAAAAAAA")) 43 ); 44 assert!( 45 parts 46 .tags 47 .contains(&tag("url", "https://media.example.invalid/blob/sha256")) 48 ); 49 assert!(parts.tags.contains(&tag("m", "image/jpeg"))); 50 assert!(parts.tags.contains(&tag("x", SHA256))); 51 52 let decoded = farm_file_metadata_from_event(parts.kind, &parts.tags, &parts.content) 53 .expect("file metadata decode"); 54 assert_eq!(decoded, metadata); 55 } 56 57 #[test] 58 fn farm_file_metadata_rejects_missing_x_bad_hash_and_missing_url() { 59 let parts = to_wire_parts(&sample_metadata()).expect("file metadata wire parts"); 60 let without_x = parts 61 .tags 62 .iter() 63 .filter(|tag| tag.first().map(|value| value.as_str()) != Some("x")) 64 .cloned() 65 .collect::<Vec<_>>(); 66 let missing_x = 67 farm_file_metadata_from_event(parts.kind, &without_x, &parts.content).unwrap_err(); 68 assert!(matches!(missing_x, EventParseError::MissingTag("x"))); 69 70 let mut bad_hash = sample_metadata(); 71 bad_hash.sha256 = "ABC".to_string(); 72 let hash_err = farm_file_metadata_build_tags(&bad_hash).unwrap_err(); 73 assert!(matches!(hash_err, EventEncodeError::InvalidField("sha256"))); 74 75 let mut missing_url = sample_metadata(); 76 missing_url.url.clear(); 77 let url_err = to_wire_parts(&missing_url).unwrap_err(); 78 assert!(matches!( 79 url_err, 80 EventEncodeError::EmptyRequiredField("url") 81 )); 82 } 83 84 #[test] 85 fn farm_file_metadata_rejects_d_mismatch_and_kind_mismatch() { 86 let parts = to_wire_parts(&sample_metadata()).expect("file metadata wire parts"); 87 let mut duplicate_d = parts.tags.clone(); 88 duplicate_d.push(vec!["d".to_string(), "AAAAAAAAAAAAAAAAAAAAAw".to_string()]); 89 let mismatch = 90 farm_file_metadata_from_event(parts.kind, &duplicate_d, &parts.content).unwrap_err(); 91 assert!(matches!(mismatch, EventParseError::InvalidTag("d"))); 92 93 let wrong_kind = to_wire_parts_with_kind(&sample_metadata(), KIND_POST).unwrap_err(); 94 assert!(matches!( 95 wrong_kind, 96 EventEncodeError::InvalidKind(KIND_POST) 97 )); 98 99 let decode_wrong_kind = 100 farm_file_metadata_from_event(KIND_POST, &parts.tags, &parts.content).unwrap_err(); 101 assert!(matches!( 102 decode_wrong_kind, 103 EventParseError::InvalidKind { 104 expected: "1063", 105 got: KIND_POST 106 } 107 )); 108 } 109 110 #[test] 111 fn farm_file_metadata_decodes_empty_content_as_absent_caption() { 112 let mut metadata = sample_metadata(); 113 metadata.caption = None; 114 let parts = to_wire_parts(&metadata).expect("file metadata wire parts"); 115 116 assert_eq!(parts.content, ""); 117 let decoded = 118 farm_file_metadata_from_event(parts.kind, &parts.tags, "").expect("file decode"); 119 assert_eq!(decoded.caption, None); 120 } 121 122 #[test] 123 fn farm_file_metadata_wrappers_roundtrip_minimal_optional_shape() { 124 let mut metadata = sample_metadata(); 125 metadata.caption = None; 126 metadata.original_sha256 = None; 127 metadata.size_bytes = None; 128 metadata.dimensions = None; 129 metadata.blurhash = None; 130 metadata.thumb = None; 131 metadata.image = Some(RadrootsFarmFileSource { 132 url: "https://media.example.invalid/image/sha256".to_string(), 133 mime_type: None, 134 dimensions: Some(RadrootsFarmFileDimensions { w: 640, h: 480 }), 135 }); 136 metadata.alt = None; 137 metadata.fallbacks.clear(); 138 let parts = to_wire_parts(&metadata).expect("file metadata wire parts"); 139 140 assert!( 141 !parts 142 .tags 143 .iter() 144 .any(|tag| tag.first().map(String::as_str) == Some("size")) 145 ); 146 assert!( 147 !parts 148 .tags 149 .iter() 150 .any(|tag| tag.first().map(String::as_str) == Some("dim")) 151 ); 152 assert!(parts.tags.iter().any(|tag| tag 153 == &vec![ 154 "image".to_string(), 155 "https://media.example.invalid/image/sha256".to_string(), 156 "640x480".to_string() 157 ])); 158 159 let data = data_from_event( 160 "event-id".to_string(), 161 "author-pubkey".to_string(), 162 42, 163 parts.kind, 164 parts.content.clone(), 165 parts.tags.clone(), 166 ) 167 .expect("parsed data"); 168 assert_eq!(data.id, "event-id"); 169 assert_eq!(data.author, "author-pubkey"); 170 assert_eq!(data.published_at, 42); 171 assert_eq!(data.kind, KIND_FARM_FILE_METADATA); 172 assert_eq!(data.data, metadata); 173 174 let err = parsed_from_event( 175 "event-id".to_string(), 176 "author-pubkey".to_string(), 177 42, 178 KIND_POST, 179 parts.content.clone(), 180 parts.tags.clone(), 181 "sig".to_string(), 182 ) 183 .unwrap_err(); 184 assert!(matches!( 185 err, 186 EventParseError::InvalidKind { 187 expected: "1063", 188 got: KIND_POST 189 } 190 )); 191 192 let parsed = parsed_from_event( 193 "event-id".to_string(), 194 "author-pubkey".to_string(), 195 42, 196 parts.kind, 197 parts.content, 198 parts.tags, 199 "sig".to_string(), 200 ) 201 .expect("parsed event"); 202 assert_eq!(parsed.event.sig, "sig"); 203 assert_eq!(parsed.data.data, metadata); 204 } 205 206 #[test] 207 fn farm_file_metadata_preserves_expanded_owner_document_kinds() { 208 for kind in [ 209 RadrootsFarmCrdtDocumentKind::FarmMembership, 210 RadrootsFarmCrdtDocumentKind::FarmRolePolicy, 211 RadrootsFarmCrdtDocumentKind::FarmActivity, 212 RadrootsFarmCrdtDocumentKind::FarmLocation, 213 RadrootsFarmCrdtDocumentKind::FarmCrop, 214 RadrootsFarmCrdtDocumentKind::FarmCropVariety, 215 RadrootsFarmCrdtDocumentKind::FarmCropCycle, 216 RadrootsFarmCrdtDocumentKind::FarmAttachment, 217 RadrootsFarmCrdtDocumentKind::FarmPayPeriod, 218 RadrootsFarmCrdtDocumentKind::Other { 219 value: "FarmSoilTest".to_string(), 220 }, 221 ] { 222 let mut metadata = sample_metadata(); 223 metadata.owner_document_kind = kind; 224 let parts = to_wire_parts(&metadata).expect("file metadata wire parts"); 225 let decoded = farm_file_metadata_from_event(parts.kind, &parts.tags, &parts.content) 226 .expect("file metadata decode"); 227 228 assert_eq!(decoded.owner_document_kind, metadata.owner_document_kind); 229 } 230 } 231 232 #[test] 233 fn farm_file_metadata_rejects_malformed_decode_tags() { 234 let parts = to_wire_parts(&sample_metadata()).expect("file metadata wire parts"); 235 236 let mut missing_owner = parts.tags.clone(); 237 missing_owner 238 .retain(|tag| tag.first().map(String::as_str) != Some("radroots:owner_document")); 239 let err = 240 farm_file_metadata_from_event(parts.kind, &missing_owner, &parts.content).unwrap_err(); 241 assert!(matches!( 242 err, 243 EventParseError::MissingTag("radroots:owner_document") 244 )); 245 246 for replacement in [ 247 vec![ 248 "radroots:owner_document".to_string(), 249 OWNER_DOCUMENT_ID.to_string(), 250 ], 251 vec![ 252 "radroots:owner_document".to_string(), 253 OWNER_DOCUMENT_ID.to_string(), 254 " ".to_string(), 255 ], 256 vec![ 257 "radroots:owner_document".to_string(), 258 "bad d tag".to_string(), 259 "FarmTask".to_string(), 260 ], 261 ] { 262 let mut tags = replace_tag(&parts.tags, "radroots:owner_document", replacement); 263 let err = farm_file_metadata_from_event(parts.kind, &tags, &parts.content).unwrap_err(); 264 assert!(matches!( 265 err, 266 EventParseError::InvalidTag("radroots:owner_document") 267 )); 268 tags.clear(); 269 } 270 271 for (key, value, expected) in [ 272 ("size", "not-a-number", "size"), 273 ("dim", "bad", "dim"), 274 ("dim", "badx12", "dim"), 275 ("dim", "12xbad", "dim"), 276 ("dim", "0x12", "dim"), 277 ("dim", "12x0", "dim"), 278 ("thumb", "", "thumb"), 279 ("thumb", " ", "thumb"), 280 ] { 281 let tags = replace_tag(&parts.tags, key, tag(key, value)); 282 let err = farm_file_metadata_from_event(parts.kind, &tags, &parts.content).unwrap_err(); 283 match err { 284 EventParseError::InvalidTag(found) | EventParseError::InvalidNumber(found, _) => { 285 assert_eq!(found, expected); 286 } 287 other => panic!("unexpected error: {other:?}"), 288 } 289 } 290 291 for replacement in [ 292 vec!["thumb".to_string()], 293 vec![ 294 "thumb".to_string(), 295 "https://media.example.invalid/thumb/sha256".to_string(), 296 "image/jpeg".to_string(), 297 "320x240".to_string(), 298 "extra".to_string(), 299 ], 300 vec![ 301 "thumb".to_string(), 302 "https://media.example.invalid/thumb/sha256".to_string(), 303 "image/jpeg".to_string(), 304 " ".to_string(), 305 ], 306 ] { 307 let tags = replace_tag(&parts.tags, "thumb", replacement); 308 let err = farm_file_metadata_from_event(parts.kind, &tags, &parts.content).unwrap_err(); 309 assert!(matches!(err, EventParseError::InvalidTag("thumb"))); 310 } 311 312 let tags = replace_tag( 313 &parts.tags, 314 "thumb", 315 vec![ 316 "thumb".to_string(), 317 "https://media.example.invalid/thumb/sha256".to_string(), 318 "320x240".to_string(), 319 ], 320 ); 321 let decoded = 322 farm_file_metadata_from_event(parts.kind, &tags, &parts.content).expect("metadata"); 323 assert_eq!( 324 decoded 325 .thumb 326 .as_ref() 327 .and_then(|source| source.mime_type.as_deref()), 328 None 329 ); 330 assert_eq!( 331 decoded.thumb.and_then(|source| source.dimensions), 332 Some(RadrootsFarmFileDimensions { w: 320, h: 240 }) 333 ); 334 335 let tags = replace_tag( 336 &parts.tags, 337 "thumb", 338 vec![ 339 "thumb".to_string(), 340 "https://media.example.invalid/thumb/sha256".to_string(), 341 ], 342 ); 343 let decoded = 344 farm_file_metadata_from_event(parts.kind, &tags, &parts.content).expect("metadata"); 345 assert_eq!(decoded.thumb.and_then(|source| source.dimensions), None); 346 347 let err = farm_file_metadata_from_event(parts.kind, &parts.tags, " ").unwrap_err(); 348 assert!(matches!(err, EventParseError::InvalidTag("caption"))); 349 } 350 351 #[test] 352 fn farm_file_metadata_rejects_encoder_validation_edges() { 353 for (metadata, expected) in [ 354 { 355 let mut metadata = sample_metadata(); 356 metadata.workspace.d_tag = "bad d tag".to_string(); 357 (metadata, EventEncodeError::InvalidField("workspace.d_tag")) 358 }, 359 { 360 let mut metadata = sample_metadata(); 361 metadata.caption = Some("".to_string()); 362 (metadata, EventEncodeError::EmptyRequiredField("caption")) 363 }, 364 { 365 let mut metadata = sample_metadata(); 366 metadata.dimensions = Some(RadrootsFarmFileDimensions { w: 0, h: 1200 }); 367 (metadata, EventEncodeError::InvalidField("dimensions")) 368 }, 369 { 370 let mut metadata = sample_metadata(); 371 metadata.blurhash = Some("".to_string()); 372 (metadata, EventEncodeError::EmptyRequiredField("blurhash")) 373 }, 374 { 375 let mut metadata = sample_metadata(); 376 metadata.thumb = Some(RadrootsFarmFileSource { 377 url: "".to_string(), 378 mime_type: None, 379 dimensions: None, 380 }); 381 (metadata, EventEncodeError::EmptyRequiredField("thumb")) 382 }, 383 { 384 let mut metadata = sample_metadata(); 385 metadata.thumb = Some(RadrootsFarmFileSource { 386 url: "https://media.example.invalid/thumb/sha256".to_string(), 387 mime_type: Some("".to_string()), 388 dimensions: None, 389 }); 390 (metadata, EventEncodeError::EmptyRequiredField("thumb")) 391 }, 392 { 393 let mut metadata = sample_metadata(); 394 metadata.thumb = Some(RadrootsFarmFileSource { 395 url: "https://media.example.invalid/thumb/sha256".to_string(), 396 mime_type: None, 397 dimensions: Some(RadrootsFarmFileDimensions { w: 320, h: 0 }), 398 }); 399 (metadata, EventEncodeError::InvalidField("thumb")) 400 }, 401 { 402 let mut metadata = sample_metadata(); 403 metadata.alt = Some("".to_string()); 404 (metadata, EventEncodeError::EmptyRequiredField("alt")) 405 }, 406 { 407 let mut metadata = sample_metadata(); 408 metadata.fallbacks = vec!["".to_string()]; 409 (metadata, EventEncodeError::EmptyRequiredField("fallbacks")) 410 }, 411 ] { 412 let err = farm_file_metadata_build_tags(&metadata).unwrap_err(); 413 assert_same_encode_error(err, expected); 414 } 415 } 416 417 fn sample_metadata() -> RadrootsFarmFileMetadata { 418 RadrootsFarmFileMetadata { 419 d_tag: FILE_D_TAG.to_string(), 420 workspace: RadrootsFarmWorkspaceRef { 421 pubkey: "workspace_pubkey".to_string(), 422 d_tag: WORKSPACE_D_TAG.to_string(), 423 }, 424 farm_group_id: GROUP_ID.to_string(), 425 owner_document_id: OWNER_DOCUMENT_ID.to_string(), 426 owner_document_kind: RadrootsFarmCrdtDocumentKind::FarmTask, 427 caption: Some("Tomatoes harvested from Patch Y.".to_string()), 428 url: "https://media.example.invalid/blob/sha256".to_string(), 429 mime_type: "image/jpeg".to_string(), 430 sha256: SHA256.to_string(), 431 original_sha256: Some( 432 "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789".to_string(), 433 ), 434 size_bytes: Some(123_456), 435 dimensions: Some(RadrootsFarmFileDimensions { w: 1600, h: 1200 }), 436 blurhash: Some("LEHV6nWB2yk8pyo0adR*.7kCMdnj".to_string()), 437 thumb: Some(RadrootsFarmFileSource { 438 url: "https://media.example.invalid/thumb/sha256".to_string(), 439 mime_type: Some("image/jpeg".to_string()), 440 dimensions: Some(RadrootsFarmFileDimensions { w: 320, h: 240 }), 441 }), 442 image: None, 443 alt: Some("Harvested tomatoes in a crate".to_string()), 444 fallbacks: vec!["https://fallback.example.invalid/blob/sha256".to_string()], 445 } 446 } 447 448 fn tag(key: &str, value: &str) -> Vec<String> { 449 vec![key.to_string(), value.to_string()] 450 } 451 452 fn replace_tag(tags: &[Vec<String>], key: &str, replacement: Vec<String>) -> Vec<Vec<String>> { 453 tags.iter() 454 .map(|tag| { 455 if tag.first().map(String::as_str) == Some(key) { 456 replacement.clone() 457 } else { 458 tag.clone() 459 } 460 }) 461 .collect() 462 } 463 464 fn assert_same_encode_error(actual: EventEncodeError, expected: EventEncodeError) { 465 match (actual, expected) { 466 ( 467 EventEncodeError::EmptyRequiredField(actual), 468 EventEncodeError::EmptyRequiredField(expected), 469 ) 470 | (EventEncodeError::InvalidField(actual), EventEncodeError::InvalidField(expected)) => { 471 assert_eq!(actual, expected); 472 } 473 (EventEncodeError::InvalidKind(actual), EventEncodeError::InvalidKind(expected)) => { 474 assert_eq!(actual, expected); 475 } 476 (EventEncodeError::Json, EventEncodeError::Json) => {} 477 (actual, expected) => panic!("unexpected error {actual:?}, expected {expected:?}"), 478 } 479 } 480 }