calendar_conformance.rs (56363B)
1 use std::{borrow::Cow, fs, path::Path}; 2 3 use radroots_blossom::{BlobDescriptor, ByteVerifiedDescriptor, Error}; 4 use radroots_event::{ 5 calendar::{ 6 AuthoredCalendar, AuthoredCalendarDateEvent, AuthoredCalendarEventRsvp, 7 AuthoredCalendarTimeEvent, CalendarAdmissionError, CalendarDate, 8 CalendarEventAuthorReference, CalendarEventError, CalendarEventFreeBusy, 9 CalendarEventReference, CalendarEventRevisionReference, CalendarEventRsvpStatus, 10 CalendarParticipant, CalendarRequest, CalendarUid, CalendarUri, IanaTimeZoneId, 11 ParsedNip52Calendar, ParsedNip52CalendarCommon, ParsedNip52CalendarEventRsvp, 12 RADROOTS_CALENDAR_MAX_COVERED_UTC_DAYS, 13 }, 14 contract::validate_event_contract_parts, 15 media::AuthoredImage, 16 }; 17 use radroots_event_codec::decode::{ 18 EventParseError, 19 calendar::{ 20 admit_radroots_calendar, admit_radroots_calendar_date_event, 21 admit_radroots_calendar_event_rsvp, admit_radroots_calendar_time_event, 22 parse_nip52_calendar, parse_nip52_calendar_date_event, parse_nip52_calendar_event_rsvp, 23 parse_nip52_calendar_time_event, 24 }, 25 }; 26 use radroots_event_codec::encode::calendar::{ 27 calendar_collection_build_tags, calendar_date_event_build_tags, calendar_time_event_build_tags, 28 calendar_to_wire_parts, date_to_wire_parts, rsvp_build_tags, rsvp_to_wire_parts, 29 time_to_wire_parts, 30 }; 31 use serde_json::{Map, Value}; 32 33 const PACKAGED_BASELINE: &str = include_str!("fixtures/calendar_nip52_baseline.v1.json"); 34 const PACKAGED_PROFILE: &str = include_str!("fixtures/calendar_radroots_profile.v1.json"); 35 const WORKSPACE_BASELINE_PATH: &str = 36 "../../contracts/conformance/vectors/calendar/nip52_baseline.v1.json"; 37 const WORKSPACE_PROFILE_PATH: &str = 38 "../../contracts/conformance/vectors/calendar/radroots_profile.v1.json"; 39 const WORKSPACE_CONTRACT_MARKER_PATH: &str = "../../contracts/manifest.toml"; 40 const BASELINE_VECTOR_KIND_COUNTS: &[(&str, usize)] = &[ 41 ("calendar.baseline.collection.invalid", 15), 42 ("calendar.baseline.collection.valid", 4), 43 ("calendar.baseline.date.invalid", 8), 44 ("calendar.baseline.date.valid", 3), 45 ("calendar.baseline.rsvp.invalid", 23), 46 ("calendar.baseline.rsvp.valid", 4), 47 ("calendar.baseline.time.invalid", 4), 48 ("calendar.baseline.time.valid", 4), 49 ]; 50 const PROFILE_VECTOR_KIND_COUNTS: &[(&str, usize)] = &[ 51 ("calendar.profile.admit.collection.invalid", 6), 52 ("calendar.profile.admit.collection.valid", 2), 53 ("calendar.profile.admit.date.invalid", 4), 54 ("calendar.profile.admit.date.valid", 1), 55 ("calendar.profile.admit.rsvp.invalid", 9), 56 ("calendar.profile.admit.rsvp.valid", 3), 57 ("calendar.profile.admit.time.invalid", 4), 58 ("calendar.profile.admit.time.valid", 1), 59 ("calendar.profile.authored.collection.invalid", 6), 60 ("calendar.profile.authored.collection.valid", 3), 61 ("calendar.profile.authored.date.invalid", 3), 62 ("calendar.profile.authored.date.valid", 1), 63 ("calendar.profile.authored.rsvp.invalid", 6), 64 ("calendar.profile.authored.rsvp.valid", 2), 65 ("calendar.profile.authored.time.coverage.valid", 1), 66 ("calendar.profile.authored.time.invalid", 2), 67 ("calendar.profile.authored.time.valid", 1), 68 ("calendar.profile.date.parse.invalid", 2), 69 ("calendar.profile.date.parse.valid", 2), 70 ]; 71 72 #[test] 73 fn checked_in_baseline_vectors_execute_against_tolerant_nip52_parsers() { 74 let suite = conformance_suite(WORKSPACE_BASELINE_PATH, PACKAGED_BASELINE); 75 assert_eq!(suite["suite"], "calendar_nip52_baseline"); 76 assert_eq!(suite["contract_version"], "1.0.0"); 77 let vectors = suite["vectors"].as_array().expect("baseline vectors"); 78 assert_vector_kind_inventory(vectors, BASELINE_VECTOR_KIND_COUNTS); 79 for vector in vectors { 80 execute_baseline(vector); 81 } 82 } 83 84 #[test] 85 fn checked_in_profile_vectors_execute_against_authored_and_admission_apis() { 86 let suite = conformance_suite(WORKSPACE_PROFILE_PATH, PACKAGED_PROFILE); 87 assert_eq!(suite["suite"], "calendar_radroots_profile"); 88 assert_eq!(suite["contract_version"], "1.0.0"); 89 let vectors = suite["vectors"].as_array().expect("profile vectors"); 90 assert_vector_kind_inventory(vectors, PROFILE_VECTOR_KIND_COUNTS); 91 for vector in vectors { 92 execute_profile(vector); 93 } 94 } 95 96 fn assert_vector_kind_inventory(vectors: &[Value], expected: &[(&str, usize)]) { 97 assert_eq!( 98 vectors.len(), 99 expected.iter().map(|(_, count)| count).sum::<usize>(), 100 "Calendar vector inventory count drifted" 101 ); 102 for (kind, expected_count) in expected { 103 let actual_count = vectors 104 .iter() 105 .filter(|vector| vector_kind(vector) == *kind) 106 .count(); 107 assert_eq!( 108 actual_count, *expected_count, 109 "Calendar vector kind {kind} inventory drifted" 110 ); 111 } 112 } 113 114 fn conformance_suite(workspace_relative_path: &str, packaged: &'static str) -> Value { 115 let workspace_path = Path::new(env!("CARGO_MANIFEST_DIR")).join(workspace_relative_path); 116 let vectors = match fs::read_to_string(&workspace_path) { 117 Ok(canonical) => { 118 assert_eq!( 119 canonical, 120 packaged, 121 "packaged Calendar vectors must match {}", 122 workspace_path.display() 123 ); 124 Cow::Owned(canonical) 125 } 126 Err(error) 127 if error.kind() == std::io::ErrorKind::NotFound 128 && !Path::new(env!("CARGO_MANIFEST_DIR")) 129 .join(WORKSPACE_CONTRACT_MARKER_PATH) 130 .is_file() => 131 { 132 Cow::Borrowed(packaged) 133 } 134 Err(error) => panic!("failed to read {}: {error}", workspace_path.display()), 135 }; 136 serde_json::from_str(&vectors).expect("Calendar vectors must parse") 137 } 138 139 fn execute_baseline(vector: &Value) { 140 match vector_kind(vector) { 141 "calendar.baseline.date.valid" => baseline_date_valid(vector), 142 "calendar.baseline.date.invalid" => baseline_date_invalid(vector), 143 "calendar.baseline.time.valid" => baseline_time_valid(vector), 144 "calendar.baseline.time.invalid" => baseline_time_invalid(vector), 145 "calendar.baseline.collection.valid" => baseline_collection_valid(vector), 146 "calendar.baseline.collection.invalid" => baseline_collection_invalid(vector), 147 "calendar.baseline.rsvp.valid" => baseline_rsvp_valid(vector), 148 "calendar.baseline.rsvp.invalid" => baseline_rsvp_invalid(vector), 149 kind => panic!( 150 "{} uses unsupported baseline kind {kind}", 151 vector_id(vector) 152 ), 153 } 154 } 155 156 fn execute_profile(vector: &Value) { 157 match vector_kind(vector) { 158 "calendar.profile.date.parse.valid" => profile_date_parse_valid(vector), 159 "calendar.profile.date.parse.invalid" => profile_date_parse_invalid(vector), 160 "calendar.profile.authored.date.valid" => profile_authored_date_valid(vector), 161 "calendar.profile.authored.date.invalid" => profile_authored_date_invalid(vector), 162 "calendar.profile.authored.time.valid" => profile_authored_time_valid(vector), 163 "calendar.profile.authored.time.coverage.valid" => { 164 profile_authored_time_coverage_valid(vector) 165 } 166 "calendar.profile.authored.time.invalid" => profile_authored_time_invalid(vector), 167 "calendar.profile.authored.collection.valid" => profile_authored_collection_valid(vector), 168 "calendar.profile.authored.collection.invalid" => { 169 profile_authored_collection_invalid(vector) 170 } 171 "calendar.profile.authored.rsvp.valid" => profile_authored_rsvp_valid(vector), 172 "calendar.profile.authored.rsvp.invalid" => profile_authored_rsvp_invalid(vector), 173 "calendar.profile.admit.date.valid" => profile_admit_date_valid(vector), 174 "calendar.profile.admit.date.invalid" => profile_admit_date_invalid(vector), 175 "calendar.profile.admit.time.valid" => profile_admit_time_valid(vector), 176 "calendar.profile.admit.time.invalid" => profile_admit_time_invalid(vector), 177 "calendar.profile.admit.collection.valid" => profile_admit_collection_valid(vector), 178 "calendar.profile.admit.collection.invalid" => profile_admit_collection_invalid(vector), 179 "calendar.profile.admit.rsvp.valid" => profile_admit_rsvp_valid(vector), 180 "calendar.profile.admit.rsvp.invalid" => profile_admit_rsvp_invalid(vector), 181 kind => panic!("{} uses unsupported profile kind {kind}", vector_id(vector)), 182 } 183 } 184 185 fn baseline_date_valid(vector: &Value) { 186 let (kind, tags, content) = inbound_parts(vector); 187 let parsed = parse_nip52_calendar_date_event(kind, &tags, content) 188 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 189 let expected = expected(vector); 190 assert_common(parsed.common(), expected, vector); 191 assert_eq!(parsed.start().as_str(), value_str(expected, "start")); 192 assert_optional_str( 193 parsed.end().map(CalendarDate::as_str), 194 expected, 195 "end", 196 vector, 197 ); 198 assert_eq!( 199 serde_json::to_value(parsed.extension_day_tags()).unwrap(), 200 expected["date_day_extensions"], 201 "{}", 202 vector_id(vector) 203 ); 204 } 205 206 fn baseline_time_valid(vector: &Value) { 207 let (kind, tags, content) = inbound_parts(vector); 208 let parsed = parse_nip52_calendar_time_event(kind, &tags, content) 209 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 210 let expected = expected(vector); 211 assert_common(parsed.common(), expected, vector); 212 assert_eq!(parsed.start_wire(), value_str(expected, "start_wire")); 213 assert_eq!(parsed.start(), value_u64(expected, "start")); 214 assert_optional_str(parsed.end_wire(), expected, "end_wire", vector); 215 assert_eq!( 216 parsed.end(), 217 expected.get("end").and_then(Value::as_u64), 218 "{}", 219 vector_id(vector) 220 ); 221 let actual_days = parsed 222 .observed_day_indices() 223 .iter() 224 .map(|day| { 225 serde_json::json!({ 226 "wire": day.wire_value(), 227 "index": day.index(), 228 "canonical": day.is_canonical(), 229 }) 230 }) 231 .collect::<Vec<_>>(); 232 assert_eq!( 233 Value::Array(actual_days), 234 expected["observed_days"], 235 "{}", 236 vector_id(vector) 237 ); 238 assert_optional_str( 239 parsed.start_tzid().map(IanaTimeZoneId::as_str), 240 expected, 241 "start_tzid", 242 vector, 243 ); 244 assert_optional_str( 245 parsed.end_tzid().map(IanaTimeZoneId::as_str), 246 expected, 247 "end_tzid", 248 vector, 249 ); 250 assert_optional_str( 251 parsed.effective_end_tzid().map(IanaTimeZoneId::as_str), 252 expected, 253 "effective_end_tzid", 254 vector, 255 ); 256 } 257 258 fn baseline_date_invalid(vector: &Value) { 259 let (kind, tags, content) = inbound_parts(vector); 260 let error = parse_nip52_calendar_date_event(kind, &tags, content) 261 .expect_err("invalid baseline date vector must fail"); 262 assert_parse_error(vector, &error); 263 } 264 265 fn baseline_time_invalid(vector: &Value) { 266 let (kind, tags, content) = inbound_parts(vector); 267 let error = parse_nip52_calendar_time_event(kind, &tags, content) 268 .expect_err("invalid baseline time vector must fail"); 269 assert_parse_error(vector, &error); 270 } 271 272 fn baseline_collection_valid(vector: &Value) { 273 let (kind, tags, content) = inbound_parts(vector); 274 let parsed = parse_nip52_calendar(kind, &tags, content) 275 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 276 assert_parsed_collection(&parsed, expected(vector), vector); 277 } 278 279 fn baseline_collection_invalid(vector: &Value) { 280 let (kind, tags, content) = inbound_parts(vector); 281 let error = parse_nip52_calendar(kind, &tags, content) 282 .expect_err("invalid baseline collection vector must fail"); 283 assert_parse_error(vector, &error); 284 } 285 286 fn baseline_rsvp_valid(vector: &Value) { 287 let (kind, tags, content) = inbound_parts(vector); 288 let parsed = parse_nip52_calendar_event_rsvp(kind, &tags, content) 289 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 290 assert_parsed_rsvp(&parsed, expected(vector), vector); 291 } 292 293 fn baseline_rsvp_invalid(vector: &Value) { 294 let (kind, tags, content) = inbound_parts(vector); 295 let error = parse_nip52_calendar_event_rsvp(kind, &tags, content) 296 .expect_err("invalid baseline RSVP vector must fail"); 297 assert_parse_error(vector, &error); 298 } 299 300 fn assert_parsed_collection(parsed: &ParsedNip52Calendar, expected: &Value, vector: &Value) { 301 assert_eq!( 302 parsed.d_tag(), 303 value_str(expected, "d"), 304 "{}", 305 vector_id(vector) 306 ); 307 assert_eq!( 308 parsed.title(), 309 value_str(expected, "title"), 310 "{}", 311 vector_id(vector) 312 ); 313 assert_eq!( 314 parsed.content(), 315 value_str(expected, "content"), 316 "{}", 317 vector_id(vector) 318 ); 319 assert_optional_str( 320 parsed.list_description(), 321 expected, 322 "list_description", 323 vector, 324 ); 325 assert_optional_str( 326 parsed.image().map(CalendarUri::as_str), 327 expected, 328 "image", 329 vector, 330 ); 331 assert_event_references( 332 parsed.event_references(), 333 expected, 334 "event_references", 335 vector, 336 ); 337 } 338 339 fn assert_parsed_rsvp(parsed: &ParsedNip52CalendarEventRsvp, expected: &Value, vector: &Value) { 340 assert_eq!( 341 parsed.d_tag(), 342 value_str(expected, "d"), 343 "{}", 344 vector_id(vector) 345 ); 346 assert_event_reference( 347 parsed.event_reference(), 348 &expected["event_reference"], 349 vector, 350 ); 351 assert_revision_reference(parsed.revision_reference(), expected, vector); 352 assert_eq!( 353 rsvp_status_str(parsed.status()), 354 value_str(expected, "status"), 355 "{}", 356 vector_id(vector) 357 ); 358 assert_optional_str( 359 parsed.observed_free_busy().map(free_busy_str), 360 expected, 361 "observed_free_busy", 362 vector, 363 ); 364 assert_optional_str( 365 parsed.effective_free_busy().map(free_busy_str), 366 expected, 367 "effective_free_busy", 368 vector, 369 ); 370 assert_author_reference(parsed.author_hint(), expected, vector); 371 assert_optional_str(parsed.note(), expected, "note", vector); 372 } 373 374 fn assert_common(common: &ParsedNip52CalendarCommon, expected: &Value, vector: &Value) { 375 assert_eq!( 376 common.d_tag(), 377 value_str(expected, "d"), 378 "{}", 379 vector_id(vector) 380 ); 381 assert_eq!( 382 common.title(), 383 value_str(expected, "title"), 384 "{}", 385 vector_id(vector) 386 ); 387 assert_optional_str(common.description(), expected, "description", vector); 388 assert_eq!( 389 serde_json::to_value(common.locations()).unwrap(), 390 expected["locations"], 391 "{}", 392 vector_id(vector) 393 ); 394 assert_optional_str(common.geohash(), expected, "geohash", vector); 395 assert_optional_str(common.summary(), expected, "summary", vector); 396 assert_optional_str( 397 common.image().map(CalendarUri::as_str), 398 expected, 399 "image", 400 vector, 401 ); 402 assert_participants(common, expected, vector); 403 assert_eq!( 404 serde_json::to_value(common.categories()).unwrap(), 405 expected["categories"], 406 "{}", 407 vector_id(vector) 408 ); 409 assert_eq!( 410 common 411 .references() 412 .iter() 413 .map(CalendarUri::as_str) 414 .collect::<Vec<_>>(), 415 expected["references"] 416 .as_array() 417 .expect("expected references") 418 .iter() 419 .map(|value| value.as_str().expect("reference")) 420 .collect::<Vec<_>>(), 421 "{}", 422 vector_id(vector) 423 ); 424 assert_calendar_requests(common, expected, vector); 425 assert_optional_str(common.legacy_name(), expected, "legacy_name", vector); 426 } 427 428 fn assert_participants(common: &ParsedNip52CalendarCommon, expected: &Value, vector: &Value) { 429 if let Some(count) = expected["participants"].as_u64() { 430 assert_eq!( 431 common.participants().len() as u64, 432 count, 433 "{}", 434 vector_id(vector) 435 ); 436 return; 437 } 438 let actual = common 439 .participants() 440 .iter() 441 .map(|participant| { 442 serde_json::json!({ 443 "pubkey": participant.pubkey, 444 "relay": participant.relay, 445 "role": participant.role, 446 }) 447 }) 448 .collect::<Vec<_>>(); 449 assert_eq!( 450 Value::Array(actual), 451 expected["participants"], 452 "{}", 453 vector_id(vector) 454 ); 455 } 456 457 fn assert_calendar_requests(common: &ParsedNip52CalendarCommon, expected: &Value, vector: &Value) { 458 if let Some(count) = expected["calendar_requests"].as_u64() { 459 assert_eq!( 460 common.calendar_requests().len() as u64, 461 count, 462 "{}", 463 vector_id(vector) 464 ); 465 return; 466 } 467 let actual = common 468 .calendar_requests() 469 .iter() 470 .map(|request| { 471 serde_json::json!({ 472 "calendar": request.calendar().as_str(), 473 "relay": request.relay(), 474 }) 475 }) 476 .collect::<Vec<_>>(); 477 assert_eq!( 478 Value::Array(actual), 479 expected["calendar_requests"], 480 "{}", 481 vector_id(vector) 482 ); 483 } 484 485 fn assert_event_references( 486 actual: &[CalendarEventReference], 487 expected: &Value, 488 key: &str, 489 vector: &Value, 490 ) { 491 let expected = expected[key] 492 .as_array() 493 .unwrap_or_else(|| panic!("{} expected.{key} must be an array", vector_id(vector))); 494 assert_eq!(actual.len(), expected.len(), "{}", vector_id(vector)); 495 for (actual, expected) in actual.iter().zip(expected) { 496 assert_event_reference(actual, expected, vector); 497 } 498 } 499 500 fn assert_event_reference(actual: &CalendarEventReference, expected: &Value, vector: &Value) { 501 let coordinate = value_str(expected, "coordinate"); 502 let relay = expected.get("relay").and_then(Value::as_str); 503 assert_eq!( 504 actual.coordinate().as_str(), 505 coordinate, 506 "{}", 507 vector_id(vector) 508 ); 509 assert_eq!(actual.relay(), relay, "{}", vector_id(vector)); 510 511 let parts = radroots_event::id::AddressableCoordinateParts::parse(coordinate) 512 .expect("expected event reference coordinate"); 513 let canonical_coordinate = format!("{}:{}:{}", parts.kind, parts.pubkey, parts.d_tag); 514 let canonical_relay = 515 relay.is_none_or(|relay| radroots_event::id::RelayUrl::parse(relay).is_ok()); 516 assert_eq!( 517 actual.is_canonical(), 518 coordinate == canonical_coordinate && canonical_relay, 519 "{} reference canonicality", 520 vector_id(vector) 521 ); 522 } 523 524 fn assert_revision_reference( 525 actual: Option<&CalendarEventRevisionReference>, 526 expected: &Value, 527 vector: &Value, 528 ) { 529 let Some(expected) = expected 530 .get("revision_reference") 531 .and_then(Value::as_object) 532 else { 533 assert!(actual.is_none(), "{} revision reference", vector_id(vector)); 534 return; 535 }; 536 let actual = actual.unwrap_or_else(|| panic!("{} revision reference", vector_id(vector))); 537 let event_id = map_str(expected, "id"); 538 let relay = expected.get("relay").and_then(Value::as_str); 539 assert_eq!(actual.raw_event_id(), event_id, "{}", vector_id(vector)); 540 assert_eq!( 541 actual.event_id().to_hex(), 542 event_id.to_ascii_lowercase(), 543 "{} normalized revision id", 544 vector_id(vector) 545 ); 546 assert_eq!(actual.relay(), relay, "{}", vector_id(vector)); 547 let canonical_relay = 548 relay.is_none_or(|relay| radroots_event::id::RelayUrl::parse(relay).is_ok()); 549 assert_eq!( 550 actual.is_canonical(), 551 event_id == actual.event_id().to_hex() && canonical_relay, 552 "{} revision canonicality", 553 vector_id(vector) 554 ); 555 } 556 557 fn assert_author_reference( 558 actual: Option<&CalendarEventAuthorReference>, 559 expected: &Value, 560 vector: &Value, 561 ) { 562 let Some(expected) = expected.get("author_hint").and_then(Value::as_object) else { 563 assert!(actual.is_none(), "{} author hint", vector_id(vector)); 564 return; 565 }; 566 let actual = actual.unwrap_or_else(|| panic!("{} author hint", vector_id(vector))); 567 let pubkey = map_str(expected, "pubkey"); 568 let relay = expected.get("relay").and_then(Value::as_str); 569 assert_eq!(actual.raw_pubkey(), pubkey, "{}", vector_id(vector)); 570 assert_eq!( 571 actual.pubkey().to_hex(), 572 pubkey.to_ascii_lowercase(), 573 "{} normalized author key", 574 vector_id(vector) 575 ); 576 assert_eq!(actual.relay(), relay, "{}", vector_id(vector)); 577 let canonical_relay = 578 relay.is_none_or(|relay| radroots_event::id::RelayUrl::parse(relay).is_ok()); 579 assert_eq!( 580 actual.is_canonical(), 581 pubkey == actual.pubkey().to_hex() && canonical_relay, 582 "{} author canonicality", 583 vector_id(vector) 584 ); 585 } 586 587 fn rsvp_status_str(status: &CalendarEventRsvpStatus) -> &'static str { 588 match status { 589 CalendarEventRsvpStatus::Accepted => "accepted", 590 CalendarEventRsvpStatus::Declined => "declined", 591 CalendarEventRsvpStatus::Tentative => "tentative", 592 } 593 } 594 595 fn free_busy_str(free_busy: &CalendarEventFreeBusy) -> &'static str { 596 match free_busy { 597 CalendarEventFreeBusy::Free => "free", 598 CalendarEventFreeBusy::Busy => "busy", 599 } 600 } 601 602 fn profile_date_parse_valid(vector: &Value) { 603 let parsed = CalendarDate::parse(input_str(vector, "value")) 604 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 605 assert_eq!(parsed.as_str(), expected_str(vector, "value")); 606 } 607 608 fn profile_date_parse_invalid(vector: &Value) { 609 let error = CalendarDate::parse(input_str(vector, "value")) 610 .expect_err("invalid profile date must fail"); 611 assert_calendar_event_error(vector, &error); 612 } 613 614 fn profile_authored_date_valid(vector: &Value) { 615 let event = authored_date_event(input(vector), vector_id(vector)) 616 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 617 let wire = date_to_wire_parts(&event) 618 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 619 assert_eq!(calendar_date_event_build_tags(&event).unwrap(), wire.tags); 620 assert!( 621 wire.tags 622 .iter() 623 .all(|tag| tag.first().map(String::as_str) != Some("D")), 624 "{}", 625 vector_id(vector) 626 ); 627 assert_wire_parts(&wire, &expected(vector)["wire_parts"], vector_id(vector)); 628 assert_authored_media_claims(vector, event.image().is_some()); 629 assert_registry_accepts( 630 wire.kind, 631 &wire.tags, 632 &wire.content, 633 "radroots.calendar.date_event.v1", 634 vector, 635 ); 636 637 let parsed = parse_nip52_calendar_date_event(wire.kind, &wire.tags, &wire.content).unwrap(); 638 admit_radroots_calendar_date_event(parsed) 639 .unwrap_or_else(|error| panic!("{} admission failed: {error}", vector_id(vector))); 640 } 641 642 fn profile_authored_date_invalid(vector: &Value) { 643 let error = authored_date_event(input(vector), vector_id(vector)) 644 .expect_err("invalid authored date event must fail"); 645 assert_calendar_event_error(vector, &error); 646 } 647 648 fn profile_authored_time_valid(vector: &Value) { 649 let event = authored_time_event(input(vector), vector_id(vector)) 650 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 651 assert_optional_str( 652 event.effective_end_tzid().map(IanaTimeZoneId::as_str), 653 expected(vector), 654 "effective_end_tzid", 655 vector, 656 ); 657 let wire = time_to_wire_parts(&event) 658 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 659 assert_eq!(calendar_time_event_build_tags(&event).unwrap(), wire.tags); 660 assert_wire_parts(&wire, &expected(vector)["wire_parts"], vector_id(vector)); 661 assert_authored_media_claims(vector, event.image().is_some()); 662 assert_registry_accepts( 663 wire.kind, 664 &wire.tags, 665 &wire.content, 666 "radroots.calendar.time_event.v1", 667 vector, 668 ); 669 670 let parsed = parse_nip52_calendar_time_event(wire.kind, &wire.tags, &wire.content).unwrap(); 671 admit_radroots_calendar_time_event(parsed) 672 .unwrap_or_else(|error| panic!("{} admission failed: {error}", vector_id(vector))); 673 } 674 675 fn profile_authored_time_coverage_valid(vector: &Value) { 676 let event = authored_time_event(input(vector), vector_id(vector)) 677 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 678 let wire = time_to_wire_parts(&event).unwrap(); 679 assert_registry_accepts( 680 wire.kind, 681 &wire.tags, 682 &wire.content, 683 "radroots.calendar.time_event.v1", 684 vector, 685 ); 686 let covered = wire 687 .tags 688 .iter() 689 .filter(|tag| tag.first().map(String::as_str) == Some("D")) 690 .collect::<Vec<_>>(); 691 assert_eq!( 692 covered.len() as u64, 693 expected_u64(vector, "count"), 694 "{}", 695 vector_id(vector) 696 ); 697 assert_eq!( 698 covered.len(), 699 RADROOTS_CALENDAR_MAX_COVERED_UTC_DAYS as usize 700 ); 701 assert_eq!(covered.first().unwrap()[1], expected_str(vector, "first")); 702 assert_eq!(covered.last().unwrap()[1], expected_str(vector, "last")); 703 for (index, tag) in covered.iter().enumerate() { 704 assert_eq!(tag[0], "D", "{}", vector_id(vector)); 705 assert_eq!(tag[1], index.to_string(), "{}", vector_id(vector)); 706 } 707 } 708 709 fn profile_authored_time_invalid(vector: &Value) { 710 let error = authored_time_event(input(vector), vector_id(vector)) 711 .expect_err("invalid authored time event must fail"); 712 assert_calendar_event_error(vector, &error); 713 } 714 715 fn profile_authored_collection_valid(vector: &Value) { 716 let calendar = authored_collection(input(vector), vector_id(vector)) 717 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 718 let wire = calendar_to_wire_parts(&calendar) 719 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 720 assert_eq!( 721 calendar_collection_build_tags(&calendar).unwrap(), 722 wire.tags, 723 "{}", 724 vector_id(vector) 725 ); 726 assert_wire_parts(&wire, &expected(vector)["wire_parts"], vector_id(vector)); 727 assert_authored_media_claims(vector, calendar.image().is_some()); 728 assert_registry_accepts( 729 wire.kind, 730 &wire.tags, 731 &wire.content, 732 "radroots.calendar.collection.v1", 733 vector, 734 ); 735 736 let parsed = parse_nip52_calendar(wire.kind, &wire.tags, &wire.content).unwrap(); 737 assert_eq!( 738 parsed.d_tag(), 739 calendar.uid().as_str(), 740 "{}", 741 vector_id(vector) 742 ); 743 assert_eq!(parsed.title(), calendar.title(), "{}", vector_id(vector)); 744 assert_eq!( 745 parsed.content(), 746 calendar.content(), 747 "{}", 748 vector_id(vector) 749 ); 750 assert_eq!( 751 parsed.event_references(), 752 calendar.event_references(), 753 "{}", 754 vector_id(vector) 755 ); 756 assert_eq!( 757 parsed.list_description(), 758 calendar.list_description(), 759 "{}", 760 vector_id(vector) 761 ); 762 assert_eq!( 763 parsed.image().map(CalendarUri::as_str), 764 calendar 765 .image() 766 .map(|image| image.descriptor().url().as_str()), 767 "{}", 768 vector_id(vector) 769 ); 770 assert!( 771 parsed 772 .event_references() 773 .iter() 774 .all(CalendarEventReference::is_canonical), 775 "{}", 776 vector_id(vector) 777 ); 778 let admitted = admit_radroots_calendar(parsed) 779 .unwrap_or_else(|error| panic!("{} admission failed: {error}", vector_id(vector))); 780 assert_eq!(admitted.uid(), calendar.uid(), "{}", vector_id(vector)); 781 assert_eq!( 782 admitted.blossom_image().is_some(), 783 calendar.image().is_some(), 784 "{}", 785 vector_id(vector) 786 ); 787 } 788 789 fn profile_authored_collection_invalid(vector: &Value) { 790 if expected_str(vector, "error") == "blob_hash_mismatch" { 791 let image = &input(vector)["image"]; 792 let error = verified_descriptor_result(image).expect_err("invalid image must fail"); 793 assert_eq!( 794 error.code(), 795 expected_str(vector, "error"), 796 "{}", 797 vector_id(vector) 798 ); 799 return; 800 } 801 let error = authored_collection(input(vector), vector_id(vector)) 802 .expect_err("invalid authored collection must fail"); 803 assert_calendar_event_error(vector, &error); 804 } 805 806 fn profile_authored_rsvp_valid(vector: &Value) { 807 let rsvp = authored_rsvp(input(vector)) 808 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 809 let wire = rsvp_to_wire_parts(&rsvp) 810 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 811 assert_eq!( 812 rsvp_build_tags(&rsvp).unwrap(), 813 wire.tags, 814 "{}", 815 vector_id(vector) 816 ); 817 assert_wire_parts(&wire, &expected(vector)["wire_parts"], vector_id(vector)); 818 assert_registry_accepts( 819 wire.kind, 820 &wire.tags, 821 &wire.content, 822 "radroots.calendar.rsvp.v1", 823 vector, 824 ); 825 assert_optional_str( 826 rsvp.observed_free_busy().map(free_busy_str), 827 expected(vector), 828 "observed_free_busy", 829 vector, 830 ); 831 assert_optional_str( 832 rsvp.effective_free_busy().map(free_busy_str), 833 expected(vector), 834 "effective_free_busy", 835 vector, 836 ); 837 838 let parsed = parse_nip52_calendar_event_rsvp(wire.kind, &wire.tags, &wire.content).unwrap(); 839 assert_eq!(parsed.d_tag(), rsvp.uid().as_str(), "{}", vector_id(vector)); 840 assert_eq!( 841 parsed.event_reference(), 842 rsvp.event_reference(), 843 "{}", 844 vector_id(vector) 845 ); 846 assert_eq!( 847 parsed.revision_reference(), 848 rsvp.revision_reference(), 849 "{}", 850 vector_id(vector) 851 ); 852 assert_eq!(parsed.status(), rsvp.status(), "{}", vector_id(vector)); 853 assert_eq!( 854 parsed.observed_free_busy(), 855 rsvp.observed_free_busy(), 856 "{}", 857 vector_id(vector) 858 ); 859 assert_eq!( 860 parsed.effective_free_busy(), 861 rsvp.effective_free_busy(), 862 "{}", 863 vector_id(vector) 864 ); 865 assert_eq!( 866 parsed.author_hint(), 867 rsvp.author_hint(), 868 "{}", 869 vector_id(vector) 870 ); 871 assert_eq!(parsed.note(), rsvp.note(), "{}", vector_id(vector)); 872 let admitted = admit_radroots_calendar_event_rsvp(parsed) 873 .unwrap_or_else(|error| panic!("{} admission failed: {error}", vector_id(vector))); 874 assert_eq!(admitted.uid(), rsvp.uid(), "{}", vector_id(vector)); 875 } 876 877 fn profile_authored_rsvp_invalid(vector: &Value) { 878 let error = authored_rsvp(input(vector)).expect_err("invalid authored RSVP must fail"); 879 assert_calendar_event_error(vector, &error); 880 } 881 882 fn profile_admit_date_valid(vector: &Value) { 883 let (kind, tags, content) = inbound_parts(vector); 884 let parsed = parse_nip52_calendar_date_event(kind, &tags, content).unwrap(); 885 let admitted = admit_radroots_calendar_date_event(parsed) 886 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 887 assert_eq!(admitted.d_tag().as_str(), expected_str(vector, "d")); 888 assert_eq!( 889 admitted.blossom_image().is_some(), 890 expected(vector)["blossom_image"].as_bool().unwrap() 891 ); 892 assert_registry_accepts( 893 kind, 894 &tags, 895 content, 896 "radroots.calendar.date_event.v1", 897 vector, 898 ); 899 } 900 901 fn profile_admit_date_invalid(vector: &Value) { 902 let (kind, tags, content) = inbound_parts(vector); 903 let parsed = parse_nip52_calendar_date_event(kind, &tags, content).unwrap(); 904 let error = admit_radroots_calendar_date_event(parsed) 905 .expect_err("invalid date admission vector must fail"); 906 assert_admission_error(vector, &error); 907 assert_registry_rejects( 908 kind, 909 &tags, 910 content, 911 "radroots.calendar.date_event.v1", 912 vector, 913 ); 914 } 915 916 fn profile_admit_time_valid(vector: &Value) { 917 let (kind, tags, content) = inbound_parts(vector); 918 let parsed = parse_nip52_calendar_time_event(kind, &tags, content).unwrap(); 919 let admitted = admit_radroots_calendar_time_event(parsed) 920 .unwrap_or_else(|error| panic!("{} failed: {error}", vector_id(vector))); 921 assert_eq!(admitted.d_tag().as_str(), expected_str(vector, "d")); 922 assert_eq!( 923 serde_json::to_value(admitted.covered_utc_days()).unwrap(), 924 expected(vector)["covered_days"] 925 ); 926 assert_eq!( 927 admitted.blossom_image().is_some(), 928 expected(vector)["blossom_image"].as_bool().unwrap() 929 ); 930 assert_registry_accepts( 931 kind, 932 &tags, 933 content, 934 "radroots.calendar.time_event.v1", 935 vector, 936 ); 937 } 938 939 fn assert_registry_accepts( 940 kind: u32, 941 tags: &[Vec<String>], 942 content: &str, 943 contract_id: &str, 944 vector: &Value, 945 ) { 946 validate_event_contract_parts(kind, tags, content, contract_id) 947 .unwrap_or_else(|error| panic!("{} registry drift: {error:?}", vector_id(vector))); 948 } 949 950 fn assert_registry_rejects( 951 kind: u32, 952 tags: &[Vec<String>], 953 content: &str, 954 contract_id: &str, 955 vector: &Value, 956 ) { 957 assert!( 958 validate_event_contract_parts(kind, tags, content, contract_id).is_err(), 959 "{} registry accepted an admission-invalid event", 960 vector_id(vector) 961 ); 962 } 963 964 fn profile_admit_time_invalid(vector: &Value) { 965 let (kind, tags, content) = inbound_parts(vector); 966 let parsed = parse_nip52_calendar_time_event(kind, &tags, content).unwrap(); 967 let error = admit_radroots_calendar_time_event(parsed) 968 .expect_err("invalid time admission vector must fail"); 969 assert_admission_error(vector, &error); 970 assert_registry_rejects( 971 kind, 972 &tags, 973 content, 974 "radroots.calendar.time_event.v1", 975 vector, 976 ); 977 } 978 979 fn profile_admit_collection_valid(vector: &Value) { 980 let (kind, tags, content) = inbound_parts(vector); 981 let parsed = parse_nip52_calendar(kind, &tags, content) 982 .unwrap_or_else(|error| panic!("{} parse failed: {error}", vector_id(vector))); 983 let admitted = admit_radroots_calendar(parsed) 984 .unwrap_or_else(|error| panic!("{} admission failed: {error}", vector_id(vector))); 985 let expected = expected(vector); 986 assert_eq!( 987 admitted.uid().as_str(), 988 value_str(expected, "d"), 989 "{}", 990 vector_id(vector) 991 ); 992 let title = tags 993 .iter() 994 .find(|tag| tag.first().map(String::as_str) == Some("title")) 995 .and_then(|tag| tag.get(1)) 996 .expect("admitted collection title tag"); 997 assert_eq!(admitted.title(), title, "{}", vector_id(vector)); 998 assert_eq!(admitted.content(), content, "{}", vector_id(vector)); 999 assert_optional_str( 1000 admitted.list_description(), 1001 expected, 1002 "list_description", 1003 vector, 1004 ); 1005 assert_event_references( 1006 admitted.event_references(), 1007 expected, 1008 "event_references", 1009 vector, 1010 ); 1011 assert_eq!( 1012 admitted.blossom_image().is_some(), 1013 expected["blossom_image"].as_bool().unwrap(), 1014 "{}", 1015 vector_id(vector) 1016 ); 1017 let image = tags 1018 .iter() 1019 .find(|tag| tag.first().map(String::as_str) == Some("image")) 1020 .and_then(|tag| tag.get(1)) 1021 .map(String::as_str); 1022 assert_eq!( 1023 admitted.parsed().image().map(CalendarUri::as_str), 1024 image, 1025 "{}", 1026 vector_id(vector) 1027 ); 1028 if let Some(level) = expected.get("media_verification") { 1029 assert_eq!(level, "structural_only", "{}", vector_id(vector)); 1030 assert!(admitted.blossom_image().is_some(), "{}", vector_id(vector)); 1031 } 1032 assert_registry_accepts( 1033 kind, 1034 &tags, 1035 content, 1036 "radroots.calendar.collection.v1", 1037 vector, 1038 ); 1039 } 1040 1041 fn profile_admit_collection_invalid(vector: &Value) { 1042 let (kind, tags, content) = inbound_parts(vector); 1043 let parsed = parse_nip52_calendar(kind, &tags, content) 1044 .unwrap_or_else(|error| panic!("{} parse failed: {error}", vector_id(vector))); 1045 let error = 1046 admit_radroots_calendar(parsed).expect_err("invalid collection admission vector must fail"); 1047 assert_admission_error(vector, &error); 1048 assert_registry_rejects( 1049 kind, 1050 &tags, 1051 content, 1052 "radroots.calendar.collection.v1", 1053 vector, 1054 ); 1055 } 1056 1057 fn profile_admit_rsvp_valid(vector: &Value) { 1058 let (kind, tags, content) = inbound_parts(vector); 1059 let parsed = parse_nip52_calendar_event_rsvp(kind, &tags, content) 1060 .unwrap_or_else(|error| panic!("{} parse failed: {error}", vector_id(vector))); 1061 let admitted = admit_radroots_calendar_event_rsvp(parsed) 1062 .unwrap_or_else(|error| panic!("{} admission failed: {error}", vector_id(vector))); 1063 let expected = expected(vector); 1064 assert_eq!( 1065 admitted.uid().as_str(), 1066 value_str(expected, "d"), 1067 "{}", 1068 vector_id(vector) 1069 ); 1070 assert_eq!( 1071 rsvp_status_str(admitted.status()), 1072 value_str(expected, "status"), 1073 "{}", 1074 vector_id(vector) 1075 ); 1076 assert_optional_str( 1077 admitted.observed_free_busy().map(free_busy_str), 1078 expected, 1079 "observed_free_busy", 1080 vector, 1081 ); 1082 assert_optional_str( 1083 admitted.effective_free_busy().map(free_busy_str), 1084 expected, 1085 "effective_free_busy", 1086 vector, 1087 ); 1088 assert_admitted_rsvp_references_against_tags(&admitted, &tags, vector); 1089 assert_eq!( 1090 admitted.note(), 1091 (!content.is_empty()).then_some(content), 1092 "{}", 1093 vector_id(vector) 1094 ); 1095 if expected.get("event_reference").is_some() { 1096 assert_event_reference( 1097 admitted.event_reference(), 1098 &expected["event_reference"], 1099 vector, 1100 ); 1101 } 1102 if expected.get("revision_reference").is_some() { 1103 assert_revision_reference(admitted.revision_reference(), expected, vector); 1104 } 1105 if expected.get("author_hint").is_some() { 1106 assert_author_reference(admitted.author_hint(), expected, vector); 1107 } 1108 assert_registry_accepts(kind, &tags, content, "radroots.calendar.rsvp.v1", vector); 1109 } 1110 1111 fn assert_admitted_rsvp_references_against_tags( 1112 admitted: &radroots_event::calendar::AdmittedCalendarEventRsvp, 1113 tags: &[Vec<String>], 1114 vector: &Value, 1115 ) { 1116 let event_tag = tags 1117 .iter() 1118 .find(|tag| tag.first().map(String::as_str) == Some("a")) 1119 .expect("admitted RSVP a tag"); 1120 assert_eq!( 1121 admitted.event_reference().coordinate().as_str(), 1122 event_tag[1], 1123 "{}", 1124 vector_id(vector) 1125 ); 1126 assert_eq!( 1127 admitted.event_reference().relay(), 1128 event_tag.get(2).map(String::as_str), 1129 "{}", 1130 vector_id(vector) 1131 ); 1132 assert!( 1133 admitted.event_reference().is_canonical(), 1134 "{}", 1135 vector_id(vector) 1136 ); 1137 1138 let revision_tag = tags 1139 .iter() 1140 .find(|tag| tag.first().map(String::as_str) == Some("e")); 1141 match (admitted.revision_reference(), revision_tag) { 1142 (Some(reference), Some(tag)) => { 1143 assert_eq!(reference.raw_event_id(), tag[1], "{}", vector_id(vector)); 1144 assert_eq!(reference.relay(), tag.get(2).map(String::as_str)); 1145 assert!(reference.is_canonical(), "{}", vector_id(vector)); 1146 } 1147 (None, None) => {} 1148 _ => panic!("{} revision reference drift", vector_id(vector)), 1149 } 1150 1151 let author_tag = tags 1152 .iter() 1153 .find(|tag| tag.first().map(String::as_str) == Some("p")); 1154 match (admitted.author_hint(), author_tag) { 1155 (Some(reference), Some(tag)) => { 1156 assert_eq!(reference.raw_pubkey(), tag[1], "{}", vector_id(vector)); 1157 assert_eq!(reference.relay(), tag.get(2).map(String::as_str)); 1158 assert!(reference.is_canonical(), "{}", vector_id(vector)); 1159 } 1160 (None, None) => {} 1161 _ => panic!("{} author hint drift", vector_id(vector)), 1162 } 1163 } 1164 1165 fn profile_admit_rsvp_invalid(vector: &Value) { 1166 let (kind, tags, content) = inbound_parts(vector); 1167 let parsed = parse_nip52_calendar_event_rsvp(kind, &tags, content) 1168 .unwrap_or_else(|error| panic!("{} parse failed: {error}", vector_id(vector))); 1169 let error = admit_radroots_calendar_event_rsvp(parsed) 1170 .expect_err("invalid RSVP admission vector must fail"); 1171 assert_admission_error(vector, &error); 1172 assert_registry_rejects(kind, &tags, content, "radroots.calendar.rsvp.v1", vector); 1173 } 1174 1175 fn authored_collection( 1176 input: &Map<String, Value>, 1177 vector_id: &str, 1178 ) -> Result<AuthoredCalendar, CalendarEventError> { 1179 let uid = CalendarUid::parse(map_str(input, "d"))?; 1180 let event_references = input["event_references"] 1181 .as_array() 1182 .expect("input.event_references") 1183 .iter() 1184 .map(calendar_event_reference) 1185 .collect::<Result<Vec<_>, _>>()?; 1186 let mut calendar = AuthoredCalendar::new( 1187 uid, 1188 map_str(input, "title"), 1189 map_optional_str(input, "content").unwrap_or_default(), 1190 event_references, 1191 )?; 1192 if let Some(description) = map_optional_str(input, "list_description") { 1193 calendar = calendar.with_list_description(description)?; 1194 } 1195 if let Some(image) = input.get("image") { 1196 calendar = calendar.with_image(authored_image(image, vector_id))?; 1197 } 1198 Ok(calendar) 1199 } 1200 1201 fn authored_rsvp( 1202 input: &Map<String, Value>, 1203 ) -> Result<AuthoredCalendarEventRsvp, CalendarEventError> { 1204 let uid = CalendarUid::parse(map_str(input, "d"))?; 1205 let event_reference = calendar_event_reference(&input["event_reference"])?; 1206 let mut rsvp = AuthoredCalendarEventRsvp::new( 1207 uid, 1208 event_reference, 1209 parse_authored_rsvp_status(map_str(input, "status")), 1210 )?; 1211 if let Some(reference) = input.get("revision_reference") { 1212 rsvp = rsvp.with_revision_reference(calendar_revision_reference(reference)?)?; 1213 } 1214 if let Some(free_busy) = map_optional_str(input, "free_busy") { 1215 rsvp = rsvp.with_free_busy(parse_authored_free_busy(free_busy))?; 1216 } 1217 if let Some(author_hint) = input.get("author_hint") { 1218 rsvp = rsvp.with_author_hint(calendar_author_reference(author_hint)?)?; 1219 } 1220 if let Some(note) = map_optional_str(input, "note") { 1221 rsvp = rsvp.with_note(note)?; 1222 } 1223 Ok(rsvp) 1224 } 1225 1226 fn calendar_event_reference(value: &Value) -> Result<CalendarEventReference, CalendarEventError> { 1227 CalendarEventReference::parse( 1228 value["coordinate"] 1229 .as_str() 1230 .expect("event_reference.coordinate"), 1231 value.get("relay").and_then(Value::as_str), 1232 ) 1233 } 1234 1235 fn calendar_revision_reference( 1236 value: &Value, 1237 ) -> Result<CalendarEventRevisionReference, CalendarEventError> { 1238 CalendarEventRevisionReference::parse( 1239 value["id"].as_str().expect("revision_reference.id"), 1240 value.get("relay").and_then(Value::as_str), 1241 ) 1242 } 1243 1244 fn calendar_author_reference( 1245 value: &Value, 1246 ) -> Result<CalendarEventAuthorReference, CalendarEventError> { 1247 CalendarEventAuthorReference::parse( 1248 value["pubkey"].as_str().expect("author_hint.pubkey"), 1249 value.get("relay").and_then(Value::as_str), 1250 ) 1251 } 1252 1253 fn parse_authored_rsvp_status(value: &str) -> CalendarEventRsvpStatus { 1254 match value { 1255 "accepted" => CalendarEventRsvpStatus::Accepted, 1256 "declined" => CalendarEventRsvpStatus::Declined, 1257 "tentative" => CalendarEventRsvpStatus::Tentative, 1258 value => panic!("unsupported authored RSVP status {value}"), 1259 } 1260 } 1261 1262 fn parse_authored_free_busy(value: &str) -> CalendarEventFreeBusy { 1263 match value { 1264 "free" => CalendarEventFreeBusy::Free, 1265 "busy" => CalendarEventFreeBusy::Busy, 1266 value => panic!("unsupported authored RSVP free/busy value {value}"), 1267 } 1268 } 1269 1270 fn authored_date_event( 1271 input: &Map<String, Value>, 1272 vector_id: &str, 1273 ) -> Result<AuthoredCalendarDateEvent, CalendarEventError> { 1274 let start = CalendarDate::parse(map_str(input, "start"))?; 1275 let mut event = 1276 AuthoredCalendarDateEvent::new(map_str(input, "d"), map_str(input, "title"), start)?; 1277 if let Some(value) = map_optional_str(input, "end") { 1278 event = event.with_end(CalendarDate::parse(value)?)?; 1279 } 1280 event = apply_authored_date_common(event, input, vector_id)?; 1281 Ok(event) 1282 } 1283 1284 fn apply_authored_date_common( 1285 mut event: AuthoredCalendarDateEvent, 1286 input: &Map<String, Value>, 1287 vector_id: &str, 1288 ) -> Result<AuthoredCalendarDateEvent, CalendarEventError> { 1289 if let Some(value) = map_optional_str(input, "description") { 1290 event = event.with_description(value)?; 1291 } 1292 if let Some(value) = map_optional_string_vec(input, "locations") { 1293 event = event.with_locations(value)?; 1294 } 1295 if let Some(value) = map_optional_str(input, "geohash") { 1296 event = event.with_geohash(value)?; 1297 } 1298 if let Some(value) = map_optional_str(input, "summary") { 1299 event = event.with_summary(value)?; 1300 } 1301 if let Some(value) = input.get("image") { 1302 event = event.with_image(authored_image(value, vector_id))?; 1303 } 1304 if let Some(value) = input.get("participants") { 1305 event = event.with_participants(participants(value, vector_id))?; 1306 } 1307 if let Some(value) = map_optional_string_vec(input, "categories") { 1308 event = event.with_categories(value)?; 1309 } 1310 if let Some(value) = map_optional_string_vec(input, "references") { 1311 event = event.with_references( 1312 value 1313 .into_iter() 1314 .map(CalendarUri::parse) 1315 .collect::<Result<Vec<_>, _>>()?, 1316 )?; 1317 } 1318 if let Some(value) = input.get("calendar_requests") { 1319 event = event.with_calendar_requests(calendar_requests(value, vector_id))?; 1320 } 1321 Ok(event) 1322 } 1323 1324 fn authored_time_event( 1325 input: &Map<String, Value>, 1326 vector_id: &str, 1327 ) -> Result<AuthoredCalendarTimeEvent, CalendarEventError> { 1328 let mut event = AuthoredCalendarTimeEvent::new( 1329 map_str(input, "d"), 1330 map_str(input, "title"), 1331 map_u64(input, "start"), 1332 )?; 1333 if let Some(value) = input.get("end").and_then(Value::as_u64) { 1334 event = event.with_end(value)?; 1335 } 1336 if let Some(value) = map_optional_str(input, "description") { 1337 event = event.with_description(value)?; 1338 } 1339 if let Some(value) = map_optional_str(input, "start_tzid") { 1340 event = event.with_start_tzid(value)?; 1341 } 1342 if let Some(value) = map_optional_str(input, "end_tzid") { 1343 event = event.with_end_tzid(value)?; 1344 } 1345 if let Some(value) = map_optional_string_vec(input, "locations") { 1346 event = event.with_locations(value)?; 1347 } 1348 if let Some(value) = map_optional_str(input, "geohash") { 1349 event = event.with_geohash(value)?; 1350 } 1351 if let Some(value) = map_optional_str(input, "summary") { 1352 event = event.with_summary(value)?; 1353 } 1354 if let Some(value) = input.get("image") { 1355 event = event.with_image(authored_image(value, vector_id))?; 1356 } 1357 if let Some(value) = input.get("participants") { 1358 event = event.with_participants(participants(value, vector_id))?; 1359 } 1360 if let Some(value) = map_optional_string_vec(input, "categories") { 1361 event = event.with_categories(value)?; 1362 } 1363 if let Some(value) = map_optional_string_vec(input, "references") { 1364 event = event.with_references( 1365 value 1366 .into_iter() 1367 .map(CalendarUri::parse) 1368 .collect::<Result<Vec<_>, _>>()?, 1369 )?; 1370 } 1371 if let Some(value) = input.get("calendar_requests") { 1372 event = event.with_calendar_requests(calendar_requests(value, vector_id))?; 1373 } 1374 Ok(event) 1375 } 1376 1377 fn authored_image(input: &Value, vector_id: &str) -> AuthoredImage { 1378 AuthoredImage::try_from(verified_descriptor(input, vector_id)) 1379 .unwrap_or_else(|error| panic!("{vector_id} image failed: {error}")) 1380 } 1381 1382 fn verified_descriptor(input: &Value, vector_id: &str) -> ByteVerifiedDescriptor { 1383 verified_descriptor_result(input) 1384 .unwrap_or_else(|error| panic!("{vector_id} byte verification failed: {error}")) 1385 } 1386 1387 fn verified_descriptor_result(input: &Value) -> Result<ByteVerifiedDescriptor, Error> { 1388 let descriptor: BlobDescriptor = 1389 serde_json::from_value(input["descriptor"].clone()).expect("image descriptor must parse"); 1390 let media_type = descriptor.media_type().clone(); 1391 descriptor.approve_reference()?.verify_bytes( 1392 input["bytes_utf8"] 1393 .as_str() 1394 .expect("image.bytes_utf8") 1395 .as_bytes(), 1396 &media_type, 1397 ) 1398 } 1399 1400 fn participants(input: &Value, vector_id: &str) -> Vec<CalendarParticipant> { 1401 input 1402 .as_array() 1403 .unwrap_or_else(|| panic!("{vector_id} participants must be an array")) 1404 .iter() 1405 .map(|value| CalendarParticipant { 1406 pubkey: value["pubkey"] 1407 .as_str() 1408 .expect("participant.pubkey") 1409 .to_string(), 1410 relay: value 1411 .get("relay") 1412 .and_then(Value::as_str) 1413 .map(str::to_string), 1414 role: value 1415 .get("role") 1416 .and_then(Value::as_str) 1417 .map(str::to_string), 1418 }) 1419 .collect() 1420 } 1421 1422 fn calendar_requests(input: &Value, vector_id: &str) -> Vec<CalendarRequest> { 1423 input 1424 .as_array() 1425 .unwrap_or_else(|| panic!("{vector_id} calendar_requests must be an array")) 1426 .iter() 1427 .map(|value| { 1428 CalendarRequest::new( 1429 value["calendar"].as_str().expect("request.calendar"), 1430 value.get("relay").and_then(Value::as_str), 1431 ) 1432 .unwrap_or_else(|error| panic!("{vector_id} request failed: {error}")) 1433 }) 1434 .collect() 1435 } 1436 1437 fn assert_authored_media_claims(vector: &Value, image_expected: bool) { 1438 let expected = expected(vector); 1439 let Some(verification) = expected.get("media_verification") else { 1440 assert!( 1441 !image_expected, 1442 "{} omits its media claim", 1443 vector_id(vector) 1444 ); 1445 return; 1446 }; 1447 assert!(image_expected, "{} claims absent media", vector_id(vector)); 1448 assert_eq!(verification, "byte_verified", "{}", vector_id(vector)); 1449 assert_eq!( 1450 expected_str(vector, "upload_completion"), 1451 "not_attested_by_codec", 1452 "{}", 1453 vector_id(vector) 1454 ); 1455 } 1456 1457 fn assert_calendar_event_error(vector: &Value, error: &CalendarEventError) { 1458 assert_eq!( 1459 error.code(), 1460 expected_str(vector, "error"), 1461 "{}", 1462 vector_id(vector) 1463 ); 1464 assert!(!error.to_string().is_empty()); 1465 if let CalendarEventError::CoveredDayLimitExceeded { max, actual } = error { 1466 assert_eq!(*max, expected_u64(vector, "max")); 1467 assert_eq!(*actual, expected_u64(vector, "actual")); 1468 } 1469 } 1470 1471 fn assert_admission_error(vector: &Value, error: &CalendarAdmissionError) { 1472 assert_eq!( 1473 error.code(), 1474 expected_str(vector, "error"), 1475 "{}", 1476 vector_id(vector) 1477 ); 1478 assert!(!error.to_string().is_empty()); 1479 if let Some(expected_field) = expected(vector).get("field").and_then(Value::as_str) { 1480 let CalendarAdmissionError::NonCanonicalField(actual_field) = error else { 1481 panic!( 1482 "{} expected a field-bearing admission error", 1483 vector_id(vector) 1484 ); 1485 }; 1486 assert_eq!(*actual_field, expected_field, "{}", vector_id(vector)); 1487 } 1488 if let CalendarAdmissionError::CoveredDayLimitExceeded { max, actual } = error { 1489 assert_eq!(*max, expected_u64(vector, "max")); 1490 assert_eq!(*actual, expected_u64(vector, "actual")); 1491 } 1492 } 1493 1494 fn assert_parse_error(vector: &Value, error: &EventParseError) { 1495 assert_eq!( 1496 error.code(), 1497 expected_str(vector, "error"), 1498 "{}", 1499 vector_id(vector) 1500 ); 1501 assert!(!error.to_string().is_empty()); 1502 if let Some(expected_tag) = expected(vector).get("tag").and_then(Value::as_str) { 1503 assert_eq!( 1504 parse_error_tag(error), 1505 Some(expected_tag), 1506 "{}", 1507 vector_id(vector) 1508 ); 1509 } 1510 } 1511 1512 fn parse_error_tag(error: &EventParseError) -> Option<&'static str> { 1513 match error { 1514 EventParseError::MissingTag(tag) 1515 | EventParseError::InvalidTag(tag) 1516 | EventParseError::DuplicateTag(tag) 1517 | EventParseError::InvalidNumber(tag, _) 1518 | EventParseError::InvalidJson(tag) => Some(tag), 1519 EventParseError::InvalidEnvelope | EventParseError::InvalidKind { .. } => None, 1520 } 1521 } 1522 1523 fn assert_wire_parts( 1524 wire: &radroots_event::wire::Nip01EventWireParts, 1525 expected: &Value, 1526 vector_id: &str, 1527 ) { 1528 assert_eq!( 1529 u64::from(wire.kind), 1530 expected["kind"].as_u64().unwrap(), 1531 "{vector_id}" 1532 ); 1533 assert_eq!( 1534 wire.content, 1535 expected["content"].as_str().unwrap(), 1536 "{vector_id}" 1537 ); 1538 assert_eq!( 1539 serde_json::to_value(&wire.tags).unwrap(), 1540 expected["tags"], 1541 "{vector_id}" 1542 ); 1543 } 1544 1545 fn inbound_parts(vector: &Value) -> (u32, Vec<Vec<String>>, &str) { 1546 let input = input(vector); 1547 let kind = map_u64(input, "kind") as u32; 1548 let tags = serde_json::from_value(input["tags"].clone()) 1549 .unwrap_or_else(|error| panic!("{} tags failed: {error}", vector_id(vector))); 1550 let content = map_str(input, "content"); 1551 (kind, tags, content) 1552 } 1553 1554 fn assert_optional_str(actual: Option<&str>, expected: &Value, key: &str, vector: &Value) { 1555 assert_eq!( 1556 actual, 1557 expected.get(key).and_then(Value::as_str), 1558 "{} expected.{key}", 1559 vector_id(vector) 1560 ); 1561 } 1562 1563 fn input(vector: &Value) -> &Map<String, Value> { 1564 vector["input"].as_object().expect("vector.input") 1565 } 1566 1567 fn expected(vector: &Value) -> &Value { 1568 &vector["expected"] 1569 } 1570 1571 fn vector_id(vector: &Value) -> &str { 1572 vector["id"].as_str().expect("vector.id") 1573 } 1574 1575 fn vector_kind(vector: &Value) -> &str { 1576 vector["kind"].as_str().expect("vector.kind") 1577 } 1578 1579 fn input_str<'a>(vector: &'a Value, field: &str) -> &'a str { 1580 map_str(input(vector), field) 1581 } 1582 1583 fn expected_str<'a>(vector: &'a Value, field: &str) -> &'a str { 1584 expected(vector)[field] 1585 .as_str() 1586 .unwrap_or_else(|| panic!("{} expected.{field}", vector_id(vector))) 1587 } 1588 1589 fn expected_u64(vector: &Value, field: &str) -> u64 { 1590 expected(vector)[field] 1591 .as_u64() 1592 .unwrap_or_else(|| panic!("{} expected.{field}", vector_id(vector))) 1593 } 1594 1595 fn value_str<'a>(value: &'a Value, field: &str) -> &'a str { 1596 value[field] 1597 .as_str() 1598 .unwrap_or_else(|| panic!("expected.{field} must be a string")) 1599 } 1600 1601 fn value_u64(value: &Value, field: &str) -> u64 { 1602 value[field] 1603 .as_u64() 1604 .unwrap_or_else(|| panic!("expected.{field} must be an integer")) 1605 } 1606 1607 fn map_str<'a>(map: &'a Map<String, Value>, field: &str) -> &'a str { 1608 map[field] 1609 .as_str() 1610 .unwrap_or_else(|| panic!("input.{field} must be a string")) 1611 } 1612 1613 fn map_optional_str<'a>(map: &'a Map<String, Value>, field: &str) -> Option<&'a str> { 1614 map.get(field).and_then(Value::as_str) 1615 } 1616 1617 fn map_u64(map: &Map<String, Value>, field: &str) -> u64 { 1618 map[field] 1619 .as_u64() 1620 .unwrap_or_else(|| panic!("input.{field} must be an integer")) 1621 } 1622 1623 fn map_optional_string_vec(map: &Map<String, Value>, field: &str) -> Option<Vec<String>> { 1624 map.get(field).map(|value| { 1625 value 1626 .as_array() 1627 .unwrap_or_else(|| panic!("input.{field} must be an array")) 1628 .iter() 1629 .map(|value| { 1630 value 1631 .as_str() 1632 .unwrap_or_else(|| panic!("input.{field} values must be strings")) 1633 .to_string() 1634 }) 1635 .collect() 1636 }) 1637 }