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