commit 3ffa691253ad594661a34c55e8965829fdf4ed71
parent 676cf3bde13d6105ebd40f346b22bb101b4e9f92
Author: triesap <tyson@radroots.org>
Date: Sun, 21 Jun 2026 20:16:59 +0000
events-codec: expand calendar coverage
- Cover calendar date/time optional paths, wrong-kind decoders, bad date tags, and timestamp parse errors.
- Add collection address validation coverage for no-relay addresses, malformed tags, non-calendar kinds, and non-address targets.
- Exercise RSVP status/free-busy variants, absent optional fields, wrong-kind decoding, and invalid event ids.
- Validate calendar integration tests, full radroots_events_codec tests, crate check, diff check, and refreshed coverage run.
Diffstat:
1 file changed, 268 insertions(+), 0 deletions(-)
diff --git a/crates/events_codec/tests/calendar.rs b/crates/events_codec/tests/calendar.rs
@@ -136,6 +136,14 @@ fn has_tag(tags: &[Vec<String>], key: &str, value: &str) -> bool {
})
}
+fn replace_tag_value(tags: &mut [Vec<String>], key: &str, value: &str) {
+ let tag = tags
+ .iter_mut()
+ .find(|tag| tag.first().map(|entry| entry.as_str()) == Some(key))
+ .expect("tag");
+ tag[1] = value.to_string();
+}
+
#[test]
fn calendar_date_event_to_wire_parts_roundtrips_tags() {
let event = sample_date_event();
@@ -389,6 +397,266 @@ fn calendar_collection_and_rsvp_reject_missing_or_invalid_required_tags() {
}
#[test]
+fn calendar_date_codecs_cover_optional_and_error_edges() {
+ let date_tags = calendar_date_event_build_tags(&sample_date_event()).unwrap();
+ let wrong_kind = calendar_date_event_from_event(KIND_POST, &date_tags, "").unwrap_err();
+ assert!(matches!(
+ wrong_kind,
+ EventParseError::InvalidKind {
+ expected: "31922",
+ got: KIND_POST
+ }
+ ));
+
+ let mut minimal = sample_date_event();
+ minimal.description = None;
+ minimal.end = None;
+ minimal.days = None;
+ minimal.location = None;
+ minimal.summary = None;
+ minimal.image = None;
+ minimal.participants = None;
+ let parts = date_to_wire_parts(&minimal).unwrap();
+ assert_eq!(parts.content, "");
+ assert!(
+ !parts
+ .tags
+ .iter()
+ .any(|tag| tag.first().map(String::as_str) == Some(TAG_D_DAY))
+ );
+ let decoded = calendar_date_event_from_event(parts.kind, &parts.tags, &parts.content).unwrap();
+ assert!(decoded.description.is_none());
+ assert!(decoded.end.is_none());
+ assert!(decoded.days.is_none());
+
+ let mut tags = calendar_date_event_build_tags(&sample_date_event()).unwrap();
+ replace_tag_value(&mut tags, TAG_END, "2026-06-19");
+ assert!(matches!(
+ calendar_date_event_from_event(KIND_CALENDAR_DATE_EVENT, &tags, ""),
+ Err(EventParseError::InvalidTag(TAG_END))
+ ));
+
+ let mut tags = calendar_date_event_build_tags(&sample_date_event()).unwrap();
+ replace_tag_value(&mut tags, TAG_D_DAY, "2026-6-20");
+ assert!(matches!(
+ calendar_date_event_from_event(KIND_CALENDAR_DATE_EVENT, &tags, ""),
+ Err(EventParseError::InvalidTag(TAG_D_DAY))
+ ));
+
+ let mut event = sample_date_event();
+ event.title.clear();
+ assert!(matches!(
+ calendar_date_event_build_tags(&event),
+ Err(EventEncodeError::EmptyRequiredField("title"))
+ ));
+
+ let mut event = sample_date_event();
+ event.end = Some("2026-6-21".to_string());
+ assert!(matches!(
+ calendar_date_event_build_tags(&event),
+ Err(EventEncodeError::InvalidField("end"))
+ ));
+
+ let mut event = sample_date_event();
+ event.end = Some("2026-06-19".to_string());
+ assert!(matches!(
+ calendar_date_event_build_tags(&event),
+ Err(EventEncodeError::InvalidField("end"))
+ ));
+}
+
+#[test]
+fn calendar_time_codecs_cover_numeric_and_validation_edges() {
+ let mut tags = calendar_time_event_build_tags(&sample_time_event()).unwrap();
+ replace_tag_value(&mut tags, TAG_START, "not-a-number");
+ assert!(matches!(
+ calendar_time_event_from_event(KIND_CALENDAR_TIME_EVENT, &tags, ""),
+ Err(EventParseError::InvalidNumber(TAG_START, _))
+ ));
+
+ let mut tags = calendar_time_event_build_tags(&sample_time_event()).unwrap();
+ replace_tag_value(&mut tags, TAG_END, "not-a-number");
+ assert!(matches!(
+ calendar_time_event_from_event(KIND_CALENDAR_TIME_EVENT, &tags, ""),
+ Err(EventParseError::InvalidNumber(TAG_END, _))
+ ));
+
+ let mut tags = calendar_time_event_build_tags(&sample_time_event()).unwrap();
+ replace_tag_value(&mut tags, TAG_D_DAY, "2026-6-20");
+ assert!(matches!(
+ calendar_time_event_from_event(KIND_CALENDAR_TIME_EVENT, &tags, ""),
+ Err(EventParseError::InvalidTag(TAG_D_DAY))
+ ));
+
+ let mut event = sample_time_event();
+ event.d_tag = "bad".to_string();
+ assert!(matches!(
+ calendar_time_event_build_tags(&event),
+ Err(EventEncodeError::InvalidField("d_tag"))
+ ));
+
+ let mut event = sample_time_event();
+ event.title.clear();
+ assert!(matches!(
+ calendar_time_event_build_tags(&event),
+ Err(EventEncodeError::EmptyRequiredField("title"))
+ ));
+
+ let mut event = sample_time_event();
+ event.dates[0].value = "2026-6-20".to_string();
+ assert!(matches!(
+ calendar_time_event_build_tags(&event),
+ Err(EventEncodeError::InvalidField("dates"))
+ ));
+}
+
+#[test]
+fn calendar_collection_codecs_cover_address_edges() {
+ let tags = calendar_collection_build_tags(&sample_calendar_collection()).unwrap();
+ let wrong_kind = calendar_from_event(KIND_POST, &tags, "").unwrap_err();
+ assert!(matches!(
+ wrong_kind,
+ EventParseError::InvalidKind {
+ expected: "31924",
+ got: KIND_POST
+ }
+ ));
+
+ let mut calendar = sample_calendar_collection();
+ calendar.summary = None;
+ calendar.image = None;
+ calendar.description = None;
+ if let RadrootsSocialTarget::Address { relays, .. } = &mut calendar.events[0] {
+ *relays = None;
+ }
+ let parts = calendar_to_wire_parts(&calendar).unwrap();
+ assert_eq!(parts.content, "");
+ let decoded = calendar_from_event(parts.kind, &parts.tags, &parts.content).unwrap();
+ assert!(decoded.description.is_none());
+ assert!(matches!(
+ &decoded.events[0],
+ RadrootsSocialTarget::Address { relays: None, .. }
+ ));
+
+ let mut tags = calendar_collection_build_tags(&sample_calendar_collection()).unwrap();
+ let address = tags
+ .iter_mut()
+ .find(|tag| tag.first().map(String::as_str) == Some(TAG_A))
+ .expect("address tag");
+ address.truncate(1);
+ assert!(matches!(
+ calendar_from_event(KIND_CALENDAR, &tags, ""),
+ Err(EventParseError::InvalidTag(TAG_A))
+ ));
+
+ let mut tags = calendar_collection_build_tags(&sample_calendar_collection()).unwrap();
+ replace_tag_value(
+ &mut tags,
+ TAG_A,
+ format!("{KIND_ARTICLE}:{EVENT_AUTHOR}:{EVENT_D_TAG}").as_str(),
+ );
+ assert!(matches!(
+ calendar_from_event(KIND_CALENDAR, &tags, ""),
+ Err(EventParseError::InvalidTag(TAG_A))
+ ));
+
+ let mut calendar = sample_calendar_collection();
+ calendar.title.clear();
+ assert!(matches!(
+ calendar_collection_build_tags(&calendar),
+ Err(EventEncodeError::EmptyRequiredField("title"))
+ ));
+
+ let mut calendar = sample_calendar_collection();
+ calendar.events[0] = RadrootsSocialTarget::Event {
+ id: EVENT_ID.to_string(),
+ author: Some(EVENT_AUTHOR.to_string()),
+ event_kind: Some(KIND_CALENDAR_TIME_EVENT),
+ relays: None,
+ };
+ assert!(matches!(
+ calendar_collection_build_tags(&calendar),
+ Err(EventEncodeError::InvalidField("events"))
+ ));
+
+ let mut calendar = sample_calendar_collection();
+ if let RadrootsSocialTarget::Address { address, .. } = &mut calendar.events[0] {
+ *address = "not-an-address".to_string();
+ }
+ assert!(matches!(
+ calendar_collection_build_tags(&calendar),
+ Err(EventEncodeError::InvalidField("events"))
+ ));
+
+ let mut calendar = sample_calendar_collection();
+ if let RadrootsSocialTarget::Address {
+ address,
+ event_kind,
+ ..
+ } = &mut calendar.events[0]
+ {
+ *address = format!("{KIND_ARTICLE}:{EVENT_AUTHOR}:{EVENT_D_TAG}");
+ *event_kind = Some(KIND_ARTICLE);
+ }
+ assert!(matches!(
+ calendar_collection_build_tags(&calendar),
+ Err(EventEncodeError::InvalidField("events"))
+ ));
+}
+
+#[test]
+fn calendar_rsvp_codecs_cover_status_and_target_edges() {
+ let tags = rsvp_build_tags(&sample_rsvp()).unwrap();
+ let wrong_kind = rsvp_from_event(KIND_POST, &tags, "").unwrap_err();
+ assert!(matches!(
+ wrong_kind,
+ EventParseError::InvalidKind {
+ expected: "31925",
+ got: KIND_POST
+ }
+ ));
+
+ let mut rsvp = sample_rsvp();
+ rsvp.status = RadrootsCalendarEventRsvpStatus::Declined;
+ rsvp.free_busy = Some(RadrootsCalendarEventFreeBusy::Free);
+ rsvp.event_id = None;
+ rsvp.note = None;
+ rsvp.participants = None;
+ if let RadrootsSocialTarget::Address { relays, .. } = &mut rsvp.event {
+ *relays = None;
+ }
+ let parts = rsvp_to_wire_parts(&rsvp).unwrap();
+ assert_eq!(parts.content, "");
+ assert!(!parts.tags.iter().any(|tag| {
+ tag.first().map(String::as_str) == Some(TAG_E)
+ || tag.first().map(String::as_str) == Some(TAG_P)
+ }));
+ let decoded = rsvp_from_event(parts.kind, &parts.tags, &parts.content).unwrap();
+ assert_eq!(decoded.status, RadrootsCalendarEventRsvpStatus::Declined);
+ assert_eq!(decoded.free_busy, Some(RadrootsCalendarEventFreeBusy::Free));
+ assert!(decoded.note.is_none());
+
+ let mut rsvp = sample_rsvp();
+ rsvp.status = RadrootsCalendarEventRsvpStatus::Tentative;
+ let parts = rsvp_to_wire_parts(&rsvp).unwrap();
+ assert!(has_tag(&parts.tags, TAG_STATUS, "tentative"));
+
+ let mut rsvp = sample_rsvp();
+ rsvp.event_id = Some("not-a-lowercase-hex-id".to_string());
+ assert!(matches!(
+ rsvp_build_tags(&rsvp),
+ Err(EventEncodeError::InvalidField("event_id"))
+ ));
+
+ let mut tags = rsvp_build_tags(&sample_rsvp()).unwrap();
+ replace_tag_value(&mut tags, TAG_E, "not-a-lowercase-hex-id");
+ assert!(matches!(
+ rsvp_from_event(KIND_CALENDAR_EVENT_RSVP, &tags, ""),
+ Err(EventParseError::InvalidTag(TAG_E))
+ ));
+}
+
+#[test]
fn calendar_wrappers_preserve_event_metadata() {
let date = sample_date_event();
let date_parts = date_to_wire_parts(&date).unwrap();