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