commit 0b9113e8edeeca1bbfe89a7e4d2eb94792c2dd02
parent 74946e23190d31c20da9348504aa5ec76c3ba7b8
Author: triesap <tyson@radroots.org>
Date: Sun, 21 Jun 2026 20:09:37 +0000
events-codec: expand farm workspace coverage
- Cover farm workspace parsed wrappers and optional farm manifests.
- Add decode rejection coverage for tag mismatches, marker tags, farm addresses, and invalid content.
- Exercise manifest validation edges for required fields, relays, media servers, supported kinds, and farm refs.
- Validate focused and full radroots_events_codec tests, crate check, diff check, and refreshed coverage run.
Diffstat:
1 file changed, 290 insertions(+), 2 deletions(-)
diff --git a/crates/events_codec/src/farm_workspace/mod.rs b/crates/events_codec/src/farm_workspace/mod.rs
@@ -14,11 +14,13 @@ mod tests {
RadrootsFarmWorkspaceManifest, RadrootsFarmWorkspaceMediaServer,
RadrootsFarmWorkspaceRelay, RadrootsFarmWorkspaceRelayMode,
},
- kinds::{KIND_FARM_FILE_METADATA, KIND_POST},
+ kinds::{KIND_FARM, KIND_FARM_FILE_METADATA, KIND_POST},
};
use crate::error::{EventEncodeError, EventParseError};
- use crate::farm_workspace::decode::farm_workspace_from_event;
+ use crate::farm_workspace::decode::{
+ data_from_event, farm_workspace_from_event, parsed_from_event,
+ };
use crate::farm_workspace::encode::{
farm_workspace_build_tags, to_wire_parts, to_wire_parts_with_kind,
};
@@ -158,6 +160,222 @@ mod tests {
));
}
+ #[test]
+ fn farm_workspace_manifest_wrappers_roundtrip_optional_farm() {
+ let mut manifest = sample_manifest();
+ manifest.farm = None;
+ manifest.media_servers.clear();
+ let parts = to_wire_parts(&manifest).expect("workspace wire parts");
+ assert!(
+ !parts
+ .tags
+ .iter()
+ .any(|tag| tag.first().map(String::as_str) == Some("a"))
+ );
+
+ let data = data_from_event(
+ "event-id".to_string(),
+ "author-pubkey".to_string(),
+ 99,
+ parts.kind,
+ parts.content.clone(),
+ parts.tags.clone(),
+ )
+ .expect("parsed data");
+ assert_eq!(data.id, "event-id");
+ assert_eq!(data.author, "author-pubkey");
+ assert_eq!(data.published_at, 99);
+ assert_eq!(data.kind, KIND_FARM_WORKSPACE_MANIFEST);
+ assert_same_manifest(&data.data, &manifest);
+
+ let parsed = parsed_from_event(
+ "event-id".to_string(),
+ "author-pubkey".to_string(),
+ 99,
+ parts.kind,
+ parts.content,
+ parts.tags,
+ "sig".to_string(),
+ )
+ .expect("parsed event");
+ assert_eq!(parsed.event.sig, "sig");
+ assert_same_manifest(&parsed.data.data, &manifest);
+ }
+
+ #[test]
+ fn farm_workspace_manifest_rejects_decode_tag_and_content_edges() {
+ let parts = to_wire_parts(&sample_manifest()).expect("workspace wire parts");
+
+ let wrong_kind =
+ farm_workspace_from_event(KIND_POST, &parts.tags, &parts.content).unwrap_err();
+ assert!(matches!(
+ wrong_kind,
+ EventParseError::InvalidKind {
+ expected: "30078",
+ got: KIND_POST
+ }
+ ));
+
+ let empty_content = farm_workspace_from_event(parts.kind, &parts.tags, " ").unwrap_err();
+ assert!(matches!(
+ empty_content,
+ EventParseError::InvalidJson("content")
+ ));
+
+ let bad_json = farm_workspace_from_event(parts.kind, &parts.tags, "{").unwrap_err();
+ assert!(matches!(bad_json, EventParseError::InvalidJson("content")));
+
+ let mut owner_mismatch = parts.tags.clone();
+ replace_first_tag(&mut owner_mismatch, "p", tag("p", "other_owner"));
+ let err =
+ farm_workspace_from_event(parts.kind, &owner_mismatch, &parts.content).unwrap_err();
+ assert!(matches!(err, EventParseError::InvalidTag("p")));
+
+ let mut missing_marker = parts.tags.clone();
+ remove_tags(&mut missing_marker, "t");
+ let err =
+ farm_workspace_from_event(parts.kind, &missing_marker, &parts.content).unwrap_err();
+ assert!(matches!(err, EventParseError::MissingTag("t")));
+
+ for replacement in [
+ tag(
+ "a",
+ &format!("{KIND_FARM}:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAg"),
+ ),
+ tag("a", "30023:farm_pubkey:AAAAAAAAAAAAAAAAAAAAAQ"),
+ tag("a", &format!("{KIND_FARM}:farm_pubkey:bad d")),
+ ] {
+ let mut tags = parts.tags.clone();
+ replace_first_tag(&mut tags, "a", replacement);
+ let err = farm_workspace_from_event(parts.kind, &tags, &parts.content).unwrap_err();
+ assert!(matches!(err, EventParseError::InvalidTag("a")));
+ }
+ }
+
+ #[test]
+ fn farm_workspace_manifest_rejects_content_validation_edges() {
+ let parts = to_wire_parts(&sample_manifest()).expect("workspace wire parts");
+
+ for (manifest, expected) in [
+ {
+ let mut manifest = sample_manifest();
+ manifest.farm_group_id.clear();
+ (manifest, EventParseError::InvalidTag("h"))
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.owner_pubkey.clear();
+ (manifest, EventParseError::InvalidTag("p"))
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.relays.clear();
+ (manifest, EventParseError::InvalidJson("relays"))
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.supported_kinds = vec![KIND_FARM_WORKSPACE_MANIFEST];
+ (manifest, EventParseError::InvalidJson("supported_kinds"))
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.name.clear();
+ (manifest, EventParseError::InvalidJson("name"))
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.protocol_version.clear();
+ (manifest, EventParseError::InvalidJson("protocol_version"))
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.relays[0].url.clear();
+ (manifest, EventParseError::InvalidJson("relays.url"))
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.media_servers[0].service.clear();
+ (
+ manifest,
+ EventParseError::InvalidJson("media_servers.service"),
+ )
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.farm.as_mut().unwrap().d_tag = "bad d".to_string();
+ (manifest, EventParseError::InvalidTag("d"))
+ },
+ ] {
+ let content = serde_json::to_string(&manifest).expect("workspace content");
+ let err = farm_workspace_from_event(parts.kind, &parts.tags, &content).unwrap_err();
+ assert_same_parse_error(err, expected);
+ }
+ }
+
+ #[test]
+ fn farm_workspace_manifest_rejects_encoder_validation_edges() {
+ for (manifest, expected) in [
+ {
+ let mut manifest = sample_manifest();
+ manifest.name.clear();
+ (manifest, EventEncodeError::EmptyRequiredField("name"))
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.owner_pubkey.clear();
+ (
+ manifest,
+ EventEncodeError::EmptyRequiredField("owner_pubkey"),
+ )
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.protocol_version.clear();
+ (
+ manifest,
+ EventEncodeError::EmptyRequiredField("protocol_version"),
+ )
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.relays[0].url.clear();
+ (manifest, EventEncodeError::EmptyRequiredField("relays.url"))
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.media_servers[0].url.clear();
+ (
+ manifest,
+ EventEncodeError::EmptyRequiredField("media_servers.url"),
+ )
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.media_servers[0].service.clear();
+ (
+ manifest,
+ EventEncodeError::EmptyRequiredField("media_servers.service"),
+ )
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.farm.as_mut().unwrap().pubkey.clear();
+ (
+ manifest,
+ EventEncodeError::EmptyRequiredField("farm.pubkey"),
+ )
+ },
+ {
+ let mut manifest = sample_manifest();
+ manifest.farm.as_mut().unwrap().d_tag = "bad d".to_string();
+ (manifest, EventEncodeError::InvalidField("farm.d_tag"))
+ },
+ ] {
+ let err = farm_workspace_build_tags(&manifest).unwrap_err();
+ assert_same_encode_error(err, expected);
+ }
+ }
+
fn sample_manifest() -> RadrootsFarmWorkspaceManifest {
RadrootsFarmWorkspaceManifest {
d_tag: D_TAG.to_string(),
@@ -191,4 +409,74 @@ mod tests {
fn tag(key: &str, value: &str) -> Vec<String> {
vec![key.to_string(), value.to_string()]
}
+
+ fn remove_tags(tags: &mut Vec<Vec<String>>, name: &str) {
+ tags.retain(|tag| tag.first().map(String::as_str) != Some(name));
+ }
+
+ fn replace_first_tag(tags: &mut [Vec<String>], name: &str, replacement: Vec<String>) {
+ let tag = tags
+ .iter_mut()
+ .find(|tag| tag.first().map(String::as_str) == Some(name))
+ .expect("tag");
+ *tag = replacement;
+ }
+
+ fn assert_same_manifest(
+ actual: &RadrootsFarmWorkspaceManifest,
+ expected: &RadrootsFarmWorkspaceManifest,
+ ) {
+ assert_eq!(
+ serde_json::to_value(actual).expect("actual manifest value"),
+ serde_json::to_value(expected).expect("expected manifest value")
+ );
+ }
+
+ fn assert_same_parse_error(actual: EventParseError, expected: EventParseError) {
+ match (actual, expected) {
+ (EventParseError::MissingTag(actual), EventParseError::MissingTag(expected))
+ | (EventParseError::InvalidTag(actual), EventParseError::InvalidTag(expected))
+ | (EventParseError::InvalidJson(actual), EventParseError::InvalidJson(expected)) => {
+ assert_eq!(actual, expected);
+ }
+ (
+ EventParseError::InvalidKind {
+ expected: actual_expected,
+ got: actual_got,
+ },
+ EventParseError::InvalidKind { expected, got },
+ ) => {
+ assert_eq!(actual_expected, expected);
+ assert_eq!(actual_got, got);
+ }
+ (
+ EventParseError::InvalidNumber(actual, _),
+ EventParseError::InvalidNumber(expected, _),
+ ) => {
+ assert_eq!(actual, expected);
+ }
+ (actual, expected) => {
+ panic!("unexpected parse error {actual:?}, expected {expected:?}")
+ }
+ }
+ }
+
+ fn assert_same_encode_error(actual: EventEncodeError, expected: EventEncodeError) {
+ match (actual, expected) {
+ (
+ EventEncodeError::EmptyRequiredField(actual),
+ EventEncodeError::EmptyRequiredField(expected),
+ )
+ | (EventEncodeError::InvalidField(actual), EventEncodeError::InvalidField(expected)) => {
+ assert_eq!(actual, expected);
+ }
+ (EventEncodeError::InvalidKind(actual), EventEncodeError::InvalidKind(expected)) => {
+ assert_eq!(actual, expected);
+ }
+ (EventEncodeError::Json, EventEncodeError::Json) => {}
+ (actual, expected) => {
+ panic!("unexpected encode error {actual:?}, expected {expected:?}")
+ }
+ }
+ }
}