mod.rs (19123B)
1 pub mod encode; 2 3 #[cfg(feature = "json")] 4 pub mod decode; 5 6 #[cfg(all(test, feature = "json"))] 7 mod tests { 8 use radroots_event::{ 9 envelope::kind::{KIND_FARM, KIND_FARM_FILE_METADATA, KIND_POST}, 10 farm::FarmRef, 11 farm::crdt::KIND_FARM_CRDT_CHANGE, 12 farm::workspace::{ 13 FarmWorkspaceManifest, FarmWorkspaceMediaServer, FarmWorkspaceRelay, 14 FarmWorkspaceRelayMode, KIND_FARM_WORKSPACE_MANIFEST, 15 RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION, RADROOTS_FARM_WORKSPACE_SCHEMA, 16 RADROOTS_FARM_WORKSPACE_TAG, 17 }, 18 }; 19 20 use crate::error::{EventEncodeError, EventParseError}; 21 use crate::farm_workspace::decode::{ 22 data_from_event, farm_workspace_from_event, parsed_from_event, 23 }; 24 use crate::farm_workspace::encode::{ 25 farm_workspace_build_tags, to_wire_parts, to_wire_parts_with_kind, 26 }; 27 28 const D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAA"; 29 const FARM_D_TAG: &str = "AAAAAAAAAAAAAAAAAAAAAQ"; 30 const GROUP_ID: &str = "field-group"; 31 const EVENT_ID: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 32 const AUTHOR: &str = crate::test_fixtures::FIXTURE_ALICE_PUBLIC_KEY_HEX; 33 const EVENT_SIG: &str = concat!( 34 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", 35 "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" 36 ); 37 38 #[test] 39 fn farm_workspace_manifest_encodes_canonical_tags_and_decodes() { 40 let manifest = sample_manifest(); 41 let parts = to_wire_parts(&manifest).expect("workspace wire parts"); 42 43 assert_eq!(parts.kind, KIND_FARM_WORKSPACE_MANIFEST); 44 assert!(parts.tags.contains(&tag("d", D_TAG))); 45 assert!(parts.tags.contains(&tag("h", GROUP_ID))); 46 assert!(parts.tags.contains(&tag("p", "workspace_owner_pubkey"))); 47 assert!(parts.tags.contains(&tag("t", RADROOTS_FARM_WORKSPACE_TAG))); 48 assert!( 49 parts 50 .tags 51 .contains(&tag("a", "30340:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAQ")) 52 ); 53 54 let decoded = farm_workspace_from_event(parts.kind, &parts.tags, &parts.content) 55 .expect("workspace decode"); 56 assert_eq!(decoded.d_tag, D_TAG); 57 assert_eq!(decoded.schema, RADROOTS_FARM_WORKSPACE_SCHEMA); 58 assert_eq!(decoded.farm_group_id, GROUP_ID); 59 assert_eq!( 60 decoded.supported_kinds, 61 vec![ 62 KIND_FARM_CRDT_CHANGE, 63 KIND_FARM_WORKSPACE_MANIFEST, 64 KIND_FARM_FILE_METADATA 65 ] 66 ); 67 } 68 69 #[test] 70 fn farm_workspace_manifest_rejects_missing_h_and_d_mismatch() { 71 let parts = to_wire_parts(&sample_manifest()).expect("workspace wire parts"); 72 let without_h = parts 73 .tags 74 .iter() 75 .filter(|tag| tag.first().map(|value| value.as_str()) != Some("h")) 76 .cloned() 77 .collect::<Vec<_>>(); 78 79 let missing_h = 80 farm_workspace_from_event(parts.kind, &without_h, &parts.content).unwrap_err(); 81 assert!(matches!(missing_h, EventParseError::MissingTag("h"))); 82 83 let mut mismatched_d = parts.tags.clone(); 84 for tag in mismatched_d.iter_mut() { 85 if tag.first().map(|value| value.as_str()) == Some("d") { 86 tag[1] = "AAAAAAAAAAAAAAAAAAAAAg".to_string(); 87 } 88 } 89 let mismatch = 90 farm_workspace_from_event(parts.kind, &mismatched_d, &parts.content).unwrap_err(); 91 assert!(matches!(mismatch, EventParseError::InvalidTag("d"))); 92 } 93 94 #[test] 95 fn farm_workspace_manifest_rejects_bad_d_tag_kind_and_schema() { 96 let mut bad_d_tag = sample_manifest(); 97 bad_d_tag.d_tag = "bad".to_string(); 98 let encode_err = farm_workspace_build_tags(&bad_d_tag).unwrap_err(); 99 assert!(matches!( 100 encode_err, 101 EventEncodeError::InvalidField("d_tag") 102 )); 103 104 let wrong_kind = to_wire_parts_with_kind(&sample_manifest(), KIND_POST).unwrap_err(); 105 assert!(matches!( 106 wrong_kind, 107 EventEncodeError::InvalidKind(KIND_POST) 108 )); 109 110 let mut bad_schema = sample_manifest(); 111 bad_schema.schema = "radroots.farm.workspace.invalid".to_string(); 112 let content = serde_json::to_string(&bad_schema).expect("bad schema content"); 113 let tags = farm_workspace_build_tags(&sample_manifest()).expect("workspace tags"); 114 let schema_err = 115 farm_workspace_from_event(KIND_FARM_WORKSPACE_MANIFEST, &tags, &content).unwrap_err(); 116 assert!(matches!(schema_err, EventParseError::InvalidJson("schema"))); 117 } 118 119 #[test] 120 fn farm_workspace_manifest_rejects_missing_field_usage_kinds_and_relays() { 121 let mut no_relays = sample_manifest(); 122 no_relays.relays.clear(); 123 let relay_err = to_wire_parts(&no_relays).unwrap_err(); 124 assert!(matches!( 125 relay_err, 126 EventEncodeError::EmptyRequiredField("relays") 127 )); 128 129 let mut unsupported = sample_manifest(); 130 unsupported.supported_kinds = vec![KIND_FARM_WORKSPACE_MANIFEST]; 131 let supported_err = to_wire_parts(&unsupported).unwrap_err(); 132 assert!(matches!( 133 supported_err, 134 EventEncodeError::InvalidField("supported_kinds") 135 )); 136 } 137 138 #[test] 139 fn farm_workspace_manifest_requires_farm_file_support_for_media_servers() { 140 let mut no_file_support = sample_manifest(); 141 no_file_support.supported_kinds = vec![KIND_FARM_CRDT_CHANGE, KIND_FARM_WORKSPACE_MANIFEST]; 142 let encode_err = to_wire_parts(&no_file_support).unwrap_err(); 143 assert!(matches!( 144 encode_err, 145 EventEncodeError::InvalidField("supported_kinds") 146 )); 147 148 let mut no_media = no_file_support.clone(); 149 no_media.media_servers.clear(); 150 let parts = to_wire_parts(&no_media).expect("non-media manifest remains valid"); 151 let decoded = farm_workspace_from_event(parts.kind, &parts.tags, &parts.content) 152 .expect("non-media manifest decodes"); 153 assert!(decoded.media_servers.is_empty()); 154 155 let mut content_missing_file_support = sample_manifest(); 156 content_missing_file_support.supported_kinds = 157 vec![KIND_FARM_CRDT_CHANGE, KIND_FARM_WORKSPACE_MANIFEST]; 158 let content = 159 serde_json::to_string(&content_missing_file_support).expect("workspace content"); 160 let tags = farm_workspace_build_tags(&sample_manifest()).expect("workspace tags"); 161 let parse_err = 162 farm_workspace_from_event(KIND_FARM_WORKSPACE_MANIFEST, &tags, &content).unwrap_err(); 163 assert!(matches!( 164 parse_err, 165 EventParseError::InvalidJson("supported_kinds") 166 )); 167 } 168 169 #[test] 170 fn farm_workspace_manifest_wrappers_roundtrip_optional_farm() { 171 let mut manifest = sample_manifest(); 172 manifest.farm = None; 173 manifest.media_servers.clear(); 174 let parts = to_wire_parts(&manifest).expect("workspace wire parts"); 175 assert!( 176 !parts 177 .tags 178 .iter() 179 .any(|tag| tag.first().map(String::as_str) == Some("a")) 180 ); 181 182 let data = data_from_event( 183 EVENT_ID.to_string(), 184 AUTHOR.to_string(), 185 99, 186 parts.kind, 187 parts.content.clone(), 188 parts.tags.clone(), 189 ) 190 .expect("parsed data"); 191 assert_eq!(data.id, EVENT_ID); 192 assert_eq!(data.author, AUTHOR); 193 assert_eq!(data.published_at, 99); 194 assert_eq!(data.kind, KIND_FARM_WORKSPACE_MANIFEST); 195 assert_same_manifest(&data.data, &manifest); 196 197 let parsed = parsed_from_event( 198 EVENT_ID.to_string(), 199 AUTHOR.to_string(), 200 99, 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_same_manifest(&parsed.data.data, &manifest); 209 } 210 211 #[test] 212 fn farm_workspace_manifest_rejects_decode_tag_and_content_edges() { 213 let parts = to_wire_parts(&sample_manifest()).expect("workspace wire parts"); 214 215 let wrong_kind = 216 farm_workspace_from_event(KIND_POST, &parts.tags, &parts.content).unwrap_err(); 217 assert!(matches!( 218 wrong_kind, 219 EventParseError::InvalidKind { 220 expected: "30078", 221 got: KIND_POST 222 } 223 )); 224 225 let empty_content = farm_workspace_from_event(parts.kind, &parts.tags, " ").unwrap_err(); 226 assert!(matches!( 227 empty_content, 228 EventParseError::InvalidJson("content") 229 )); 230 231 let bad_json = farm_workspace_from_event(parts.kind, &parts.tags, "{").unwrap_err(); 232 assert!(matches!(bad_json, EventParseError::InvalidJson("content"))); 233 234 let mut owner_mismatch = parts.tags.clone(); 235 replace_first_tag(&mut owner_mismatch, "p", tag("p", "other_owner")); 236 let err = 237 farm_workspace_from_event(parts.kind, &owner_mismatch, &parts.content).unwrap_err(); 238 assert!(matches!(err, EventParseError::InvalidTag("p"))); 239 240 let mut missing_marker = parts.tags.clone(); 241 remove_tags(&mut missing_marker, "t"); 242 let err = 243 farm_workspace_from_event(parts.kind, &missing_marker, &parts.content).unwrap_err(); 244 assert!(matches!(err, EventParseError::MissingTag("t"))); 245 246 for replacement in [ 247 tag( 248 "a", 249 &format!("{KIND_FARM}:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAg"), 250 ), 251 tag("a", "30023:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAQ"), 252 tag("a", &format!("{KIND_FARM}:farm_pubkey:bad d")), 253 ] { 254 let mut tags = parts.tags.clone(); 255 replace_first_tag(&mut tags, "a", replacement); 256 let err = farm_workspace_from_event(parts.kind, &tags, &parts.content).unwrap_err(); 257 assert!(matches!(err, EventParseError::InvalidTag("a"))); 258 } 259 } 260 261 #[test] 262 fn farm_workspace_manifest_rejects_content_validation_edges() { 263 let parts = to_wire_parts(&sample_manifest()).expect("workspace wire parts"); 264 265 for (manifest, expected) in [ 266 { 267 let mut manifest = sample_manifest(); 268 manifest.farm_group_id.clear(); 269 (manifest, EventParseError::InvalidTag("h")) 270 }, 271 { 272 let mut manifest = sample_manifest(); 273 manifest.owner_pubkey.clear(); 274 (manifest, EventParseError::InvalidTag("p")) 275 }, 276 { 277 let mut manifest = sample_manifest(); 278 manifest.relays.clear(); 279 (manifest, EventParseError::InvalidJson("relays")) 280 }, 281 { 282 let mut manifest = sample_manifest(); 283 manifest.supported_kinds = vec![KIND_FARM_WORKSPACE_MANIFEST]; 284 (manifest, EventParseError::InvalidJson("supported_kinds")) 285 }, 286 { 287 let mut manifest = sample_manifest(); 288 manifest.name.clear(); 289 (manifest, EventParseError::InvalidJson("name")) 290 }, 291 { 292 let mut manifest = sample_manifest(); 293 manifest.protocol_version.clear(); 294 (manifest, EventParseError::InvalidJson("protocol_version")) 295 }, 296 { 297 let mut manifest = sample_manifest(); 298 manifest.relays[0].url.clear(); 299 (manifest, EventParseError::InvalidJson("relays.url")) 300 }, 301 { 302 let mut manifest = sample_manifest(); 303 manifest.media_servers[0].service.clear(); 304 ( 305 manifest, 306 EventParseError::InvalidJson("media_servers.service"), 307 ) 308 }, 309 { 310 let mut manifest = sample_manifest(); 311 manifest.farm.as_mut().unwrap().d_tag = "bad d".to_string(); 312 (manifest, EventParseError::InvalidTag("d")) 313 }, 314 ] { 315 let content = serde_json::to_string(&manifest).expect("workspace content"); 316 let err = farm_workspace_from_event(parts.kind, &parts.tags, &content).unwrap_err(); 317 assert_same_parse_error(err, expected); 318 } 319 } 320 321 #[test] 322 fn farm_workspace_manifest_rejects_encoder_validation_edges() { 323 for (manifest, expected) in [ 324 { 325 let mut manifest = sample_manifest(); 326 manifest.name.clear(); 327 (manifest, EventEncodeError::EmptyRequiredField("name")) 328 }, 329 { 330 let mut manifest = sample_manifest(); 331 manifest.owner_pubkey.clear(); 332 ( 333 manifest, 334 EventEncodeError::EmptyRequiredField("owner_pubkey"), 335 ) 336 }, 337 { 338 let mut manifest = sample_manifest(); 339 manifest.protocol_version.clear(); 340 ( 341 manifest, 342 EventEncodeError::EmptyRequiredField("protocol_version"), 343 ) 344 }, 345 { 346 let mut manifest = sample_manifest(); 347 manifest.relays[0].url.clear(); 348 (manifest, EventEncodeError::EmptyRequiredField("relays.url")) 349 }, 350 { 351 let mut manifest = sample_manifest(); 352 manifest.media_servers[0].url.clear(); 353 ( 354 manifest, 355 EventEncodeError::EmptyRequiredField("media_servers.url"), 356 ) 357 }, 358 { 359 let mut manifest = sample_manifest(); 360 manifest.media_servers[0].service.clear(); 361 ( 362 manifest, 363 EventEncodeError::EmptyRequiredField("media_servers.service"), 364 ) 365 }, 366 { 367 let mut manifest = sample_manifest(); 368 manifest.farm.as_mut().unwrap().pubkey.clear(); 369 ( 370 manifest, 371 EventEncodeError::EmptyRequiredField("farm.pubkey"), 372 ) 373 }, 374 { 375 let mut manifest = sample_manifest(); 376 manifest.farm.as_mut().unwrap().d_tag = "bad d".to_string(); 377 (manifest, EventEncodeError::InvalidField("farm.d_tag")) 378 }, 379 ] { 380 let err = farm_workspace_build_tags(&manifest).unwrap_err(); 381 assert_same_encode_error(err, expected); 382 } 383 } 384 385 fn sample_manifest() -> FarmWorkspaceManifest { 386 FarmWorkspaceManifest { 387 d_tag: D_TAG.to_string(), 388 schema: RADROOTS_FARM_WORKSPACE_SCHEMA.to_string(), 389 farm_group_id: GROUP_ID.to_string(), 390 name: "Small Regen Farm".to_string(), 391 owner_pubkey: "workspace_owner_pubkey".to_string(), 392 farm: Some(FarmRef { 393 pubkey: "farm_pubkey".to_string(), 394 d_tag: FARM_D_TAG.to_string(), 395 }), 396 relays: vec![FarmWorkspaceRelay { 397 url: "wss://relay.example.invalid/farm/field-group".to_string(), 398 mode: FarmWorkspaceRelayMode::ReadWrite, 399 }], 400 media_servers: vec![FarmWorkspaceMediaServer { 401 url: "https://media.example.invalid/farm/field-group".to_string(), 402 service: "RadrootsPrivateMedia".to_string(), 403 }], 404 supported_kinds: vec![ 405 KIND_FARM_CRDT_CHANGE, 406 KIND_FARM_WORKSPACE_MANIFEST, 407 KIND_FARM_FILE_METADATA, 408 ], 409 protocol_version: RADROOTS_FARM_WORKSPACE_PROTOCOL_VERSION.to_string(), 410 created_at_ms: 1_780_000_000_000, 411 updated_at_ms: None, 412 } 413 } 414 415 fn tag(key: &str, value: &str) -> Vec<String> { 416 vec![key.to_string(), value.to_string()] 417 } 418 419 fn remove_tags(tags: &mut Vec<Vec<String>>, name: &str) { 420 tags.retain(|tag| tag.first().map(String::as_str) != Some(name)); 421 } 422 423 fn replace_first_tag(tags: &mut [Vec<String>], name: &str, replacement: Vec<String>) { 424 let tag = tags 425 .iter_mut() 426 .find(|tag| tag.first().map(String::as_str) == Some(name)) 427 .expect("tag"); 428 *tag = replacement; 429 } 430 431 fn assert_same_manifest(actual: &FarmWorkspaceManifest, expected: &FarmWorkspaceManifest) { 432 assert_eq!( 433 serde_json::to_value(actual).expect("actual manifest value"), 434 serde_json::to_value(expected).expect("expected manifest value") 435 ); 436 } 437 438 #[cfg_attr(coverage_nightly, coverage(off))] 439 fn assert_same_parse_error(actual: EventParseError, expected: EventParseError) { 440 match (actual, expected) { 441 (EventParseError::MissingTag(actual), EventParseError::MissingTag(expected)) 442 | (EventParseError::InvalidTag(actual), EventParseError::InvalidTag(expected)) 443 | (EventParseError::InvalidJson(actual), EventParseError::InvalidJson(expected)) => { 444 assert_eq!(actual, expected); 445 } 446 ( 447 EventParseError::InvalidKind { 448 expected: actual_expected, 449 got: actual_got, 450 }, 451 EventParseError::InvalidKind { expected, got }, 452 ) => { 453 assert_eq!(actual_expected, expected); 454 assert_eq!(actual_got, got); 455 } 456 ( 457 EventParseError::InvalidNumber(actual, _), 458 EventParseError::InvalidNumber(expected, _), 459 ) => { 460 assert_eq!(actual, expected); 461 } 462 (actual, expected) => { 463 panic!("unexpected parse error {actual:?}, expected {expected:?}") 464 } 465 } 466 } 467 468 #[cfg_attr(coverage_nightly, coverage(off))] 469 fn assert_same_encode_error(actual: EventEncodeError, expected: EventEncodeError) { 470 match (actual, expected) { 471 ( 472 EventEncodeError::EmptyRequiredField(actual), 473 EventEncodeError::EmptyRequiredField(expected), 474 ) 475 | (EventEncodeError::InvalidField(actual), EventEncodeError::InvalidField(expected)) => { 476 assert_eq!(actual, expected); 477 } 478 (EventEncodeError::InvalidKind(actual), EventEncodeError::InvalidKind(expected)) => { 479 assert_eq!(actual, expected); 480 } 481 (EventEncodeError::Json, EventEncodeError::Json) => {} 482 (actual, expected) => { 483 panic!("unexpected encode error {actual:?}, expected {expected:?}") 484 } 485 } 486 } 487 }